diff options
34 files changed, 601 insertions, 313 deletions
diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl index 99b3923b2..aece47a66 100644 --- a/data/templates/accel-ppp/pppoe.config.tmpl +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -114,12 +114,14 @@ dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{ {% endif -%} {% endif %} +{% if sesscrtl != 'disable' %} +[common] +single-session={{ sesscrtl }} +{% endif %} + [ppp] verbose=1 check-ip=1 -{% if not sesscrtl == 'disable' %} -single-session={{sesscrtl}} -{% endif -%} {% if ppp_ccp %} ccp=1 {% endif %} diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl new file mode 100644 index 000000000..b5a10c3b8 --- /dev/null +++ b/data/templates/dhcp-client/daemon-options.tmpl @@ -0,0 +1 @@ +DHCLIENT_OPTS="-nw -cf {{ conf_file }} -pf {{ pid_file }} -lf {{ lease_file }} {{ '-S' if dhcpv6_prm_only }} {{ '-T' if dhcpv6_temporary }} {{ ifname }}"
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index 43f273077..ab772b5f6 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -1,4 +1,4 @@ -# generated by ifconfig.py +# generated by dhcp.py option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; timeout 60; retry 300; diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl index 83db40c5f..be0235add 100644 --- a/data/templates/dhcp-client/ipv6.tmpl +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -1,4 +1,4 @@ -# generated by ifconfig.py +# generated by dhcp.py interface "{{ ifname }}" { request routers, domain-name-servers, domain-name; } diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl index 80d620fcf..d6b0ae935 100644 --- a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl +++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl @@ -21,7 +21,7 @@ shared-network {{ network.name }} { range6 {{ range.start }} {{ range.stop }}; {%- endfor %} {%- if subnet.domain_search %} - option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; + option dhcp6.domain-search "{{ subnet.domain_search | join('", "') }}"; {%- endif %} {%- if subnet.lease_def %} default-lease-time {{ subnet.lease_def }}; @@ -51,7 +51,7 @@ shared-network {{ network.name }} { option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; {%- endif %} {%- if subnet.sip_hostname %} - option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; + option dhcp6.sip-servers-names "{{ subnet.sip_hostname | join('", "') }}"; {%- endif %} {%- if subnet.sntp_server %} option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index 7d4c0de23..4073b46b2 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -126,16 +126,37 @@ <leafNode name="default"> <properties> <help>Default time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>DHCPv6 valid lifetime</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> <leafNode name="maximum"> <properties> <help>Maximum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Maximum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> <leafNode name="minimum"> <properties> <help>Minimum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Minimum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> </children> @@ -243,29 +264,24 @@ </tagNode> </children> </node> - <leafNode name="sip-server-address"> + <leafNode name="sip-server"> <properties> <help>IPv6 address of SIP server</help> <valueHelp> <format>ipv6</format> <description>IPv6 address of SIP server</description> </valueHelp> + <valueHelp> + <format>hostname</format> + <description>FQDN of SIP server</description> + </valueHelp> <constraint> <validator name="ipv6-address"/> + <validator name="fqdn"/> </constraint> <multi/> </properties> </leafNode> - <leafNode name="sip-server-name"> - <properties> - <help>SIP server name</help> - <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> - </constraint> - <constraintErrorMessage>Invalid SIP server name. May only contain letters, numbers and .-_</constraintErrorMessage> - <multi/> - </properties> - </leafNode> <leafNode name="sntp-server"> <properties> <help>IPv6 address of an SNTP server for client to use</help> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml index d623fe103..1aea8eef6 100644 --- a/op-mode-definitions/traceroute.xml +++ b/op-mode-definitions/traceroute.xml @@ -1,71 +1,70 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="traceroute"> + <tagNode name="traceroute"> <properties> <help>Track network path to node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> </properties> + <command>/usr/bin/traceroute "$2"</command> + </tagNode> + <node name="traceroute"> <children> - <tagNode name=""> - <properties> - <help>Track network path to specified node</help> - <completionHelp> - <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> - </completionHelp> - </properties> - <command>/usr/bin/traceroute $2</command> - </tagNode> <tagNode name="ipv4"> <properties> - <help>Track network path to <hostname|IPv4 address></help> + <help>Explicitly use IPv4 when tracing the path</help> <completionHelp> <list><hostname> <x.x.x.x></list> </completionHelp> </properties> - <command>/usr/bin/traceroute -4 $3</command> + <command>/usr/bin/traceroute -4 "$3"</command> </tagNode> <tagNode name="ipv6"> <properties> - <help>Track network path to <hostname|IPv6 address></help> + <help>Explicitly use IPv6 when tracing the path</help> <completionHelp> <list><hostname> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>/usr/bin/traceroute -6 $3</command> + <command>/usr/bin/traceroute -6 "$3"</command> </tagNode> <tagNode name="vrf"> <properties> - <help>Track network path to specified node via given VRF instance</help> + <help>Track network path to specified node via given VRF</help> <completionHelp> <path>vrf name</path> </completionHelp> </properties> <children> + <!-- we need an empty tagNode to pass in a plain fqdn/ip address and + let traceroute decide how to handle this parameter --> <tagNode name=""> <properties> - <help>Track network path to specified node</help> + <help>Track network path to specified node via given VRF</help> <completionHelp> <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>sudo ip vrf exec "$3" traceroute "$4"</command> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute "$4"</command> </tagNode> <tagNode name="ipv4"> <properties> - <help>Track network path to <hostname|IPv4 address></help> + <help>Explicitly use IPv4 when tracing the path via given VRF</help> <completionHelp> <list><hostname> <x.x.x.x></list> </completionHelp> </properties> - <command>sudo ip vrf exec "$3" traceroute -4 "$5"</command> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command> </tagNode> <tagNode name="ipv6"> <properties> - <help>Track network path to <hostname|IPv6 address></help> + <help>Explicitly use IPv6 when tracing the path via given VRF</help> <completionHelp> <list><hostname> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>sudo ip vrf exec "$3" traceroute -6 "$5"</command> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command> </tagNode> </children> </tagNode> @@ -75,13 +74,38 @@ <children> <tagNode name="traceroute"> <properties> - <help>Monitor the path to a destination in realtime</help> + <help>Monitor path to destination in realtime</help> <completionHelp> <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>/usr/bin/mtr $3</command> + <command>/usr/bin/mtr "$3"</command> </tagNode> + <node name="traceroute"> + <children> + <tagNode name="vrf"> + <properties> + <help>Monitor path to destination in realtime via given VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <!-- we need an empty tagNode to pass in a plain fqdn/ip address and + let traceroute decide how to handle this parameter --> + <tagNode name=""> + <properties> + <help>Track network path to specified node via given VRF</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr "$5"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 6698aa404..b7838d8a2 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -26,6 +26,17 @@ from vyos.version import get_full_version_data DISABLE = False +_noteworthy = [] + +def noteworthy(msg): + """ + noteworthy can be use to take note things which we may not want to + report to the user may but be worth including in bug report + if something goes wrong later on + """ + _noteworthy.append(msg) + + # emulate a file object class _IO(object): def __init__(self, std, log): @@ -58,11 +69,16 @@ def bug_report(dtype, value, trace): information = get_full_version_data() trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n') + note = '' + if _noteworthy: + note = 'noteworthy:\n' + note += '\n'.join(_noteworthy) information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'trace': trace, 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'note': note, }) sys.stdout.write(INTRO.format(**information)) @@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial} Hardware UUID: {hardware_uuid} {trace} +{note} """ INTRO = """\ diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index 3122147a3..bf6566c07 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -19,28 +19,20 @@ from vyos.dicts import FixedDict from vyos.ifconfig.control import Control from vyos.template import render +config_base = r'/var/lib/dhcp/dhclient_' -class _DHCP (Control): - client_base = r'/var/lib/dhcp/dhclient_' - - def __init__(self, ifname, version, **kargs): - super().__init__(**kargs) - 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', - } - -class _DHCPv4 (_DHCP): +class _DHCPv4 (Control): def __init__(self, ifname): - super().__init__(ifname, '') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, 'hostname': '', 'client_id': '', - 'vendor_class_id': '' + 'vendor_class_id': '', + 'conf_file': config_base + f'{ifname}.conf', + 'options_file': config_base + f'{ifname}.options', + 'pid_file': config_base + f'{ifname}.pid', + 'lease_file': config_base + f'{ifname}.leases', }) # replace dhcpv4/v6 with systemd.networkd? @@ -55,25 +47,16 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.set() """ - if not self.options['hostname']: # read configured system hostname. # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: self.options['hostname'] = f.read().rstrip('\n') - render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - 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.file)) + return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -86,44 +69,29 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCP client PID found') return None - # 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: - # 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.file)) + self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) - -class _DHCPv6 (_DHCP): +class _DHCPv6 (Control): def __init__(self, ifname): - super().__init__(ifname, 'v6') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, + 'conf_file': config_base + f'v6_{ifname}.conf', + 'options_file': config_base + f'v6_{ifname}.options', + 'pid_file': config_base + f'v6_{ifname}.pid', + 'lease_file': config_base + f'v6_{ifname}.leases', 'dhcpv6_prm_only': False, 'dhcpv6_temporary': False, }) - self.file.update({ - 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', - }) def set(self): """ @@ -134,7 +102,7 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.set_dhcpv6() + >>> j.dhcp.v6.set() """ # better save then sorry .. should be checked in interface script @@ -143,29 +111,13 @@ class _DHCPv6 (_DHCP): raise Exception( 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv6.tmpl', self.options) # no longer accept router announcements on this interface - self._write_sysfs(self.file['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 self.options['dhcpv6_prm_only']: - cmd += ' -S' - if self.options['dhcpv6_temporary']: - cmd += ' -T' - cmd += ' {ifname}' - - return self._cmd(cmd.format(**self.file)) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 0) + + return self._cmd('systemctl restart dhclient6@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -176,33 +128,24 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.del_dhcpv6() + >>> j.dhcp.v6.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCPv6 client PID found') return None - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient - cmd = 'start-stop-daemon' - cmd += ' --stop' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient6@{ifname}.service'.format(**self.options)) # accept router announcements on this interface - self._write_sysfs(self.file['accept_ra'], 1) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 1) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) -class DHCP (object): +class DHCP(object): def __init__(self, ifname): self.v4 = _DHCPv4(ifname) self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/template.py b/python/vyos/template.py index 6c73ce753..e4b253ed3 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -19,6 +19,7 @@ from jinja2 import Environment from jinja2 import FileSystemLoader from vyos.defaults import directories +from vyos.util import chmod, chown, makedir # reuse the same Environment to improve performance @@ -32,7 +33,7 @@ _templates_mem = { } -def render(destination, template, content, trim_blocks=False, formater=None): +def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): """ render a template from the template directory, it will raise on any errors destination: the file where the rendered template must be saved @@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None): (recovering the load time and overhead caused by having the file out of the code) """ + # Create the directory if it does not exists + folder = os.path.dirname(destination) + makedir(folder, user, group) + # Setup a renderer for the given template # This is cached and re-used for performance if template not in _templates_mem[trim_blocks]: @@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None): # Write client config file with open(destination, 'w') as f: f.write(content) + + chmod(destination, permission) + chown(destination, user, group) diff --git a/python/vyos/util.py b/python/vyos/util.py index 4340332d3..92b6f7992 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import sys # # NOTE: Do not import full classes here, move your import to the function @@ -25,7 +26,7 @@ import os # which all have slighty different behaviour from subprocess import Popen, PIPE, STDOUT, DEVNULL def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ popen is a wrapper helper aound subprocess.Popen with it default setting it will return a tuple (out, err) @@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - STDOUT, send the data to be merged with stdout - DEVNULL, discard the output decode: specify the expected text encoding (utf-8, ascii, ...) + the default is explicitely utf-8 which is python's own default usage: to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT) to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE) """ from vyos import debug + from vyos import airbag # log if the flag is set, otherwise log if command is set if not debug.enabled(flag): flag = 'command' @@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, stdin=stdin, stdout=stdout, stderr=stderr, env=env, shell=use_shell, ) - tmp = p.communicate(input, timeout) - out1 = b'' - out2 = b'' + + pipe = p.communicate(input, timeout) + + pipe_out = b'' if stdout == PIPE: - out1 = tmp[0] + pipe_out = pipe[0] + + pipe_err = b'' if stderr == PIPE: - out2 += tmp[1] - decoded1 = out1.decode(decode) if decode else out1.decode() - decoded2 = out2.decode(decode) if decode else out2.decode() - decoded1 = decoded1.replace('\r\n', '\n').strip() - decoded2 = decoded2.replace('\r\n', '\n').strip() - nl = '\n' if decoded1 and decoded2 else '' - decoded = decoded1 + nl + decoded2 - if decoded: - ret_msg = f"returned:\n{decoded}" - debug.message(ret_msg, flag) - return decoded, p.returncode + pipe_err = pipe[1] + + str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip() + str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip() + + out_msg = f"returned (out):\n{str_out}" + if str_out: + debug.message(out_msg, flag) + + if str_err: + err_msg = f"returned (err):\n{str_err}" + # this message will also be send to syslog via airbag + debug.message(err_msg, flag, destination=sys.stderr) + + # should something go wrong, report this too via airbag + airbag.noteworthy(cmd_msg) + airbag.noteworthy(out_msg) + airbag.noteworthy(err_msg) + + return str_out, p.returncode def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=None, decode=None): + stdout=DEVNULL, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which discard the stdout and will return the error code of a command @@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None, - raising=None, message=''): + stdout=PIPE, stderr=PIPE, decode='utf-8', + raising=None, message='', expect=[0]): """ A wrapper around vyos.util.popen, which returns the stdout and will raise the error code of a command raising: specify which call should be used when raising (default is OSError) the class should only require a string as parameter + expect: a list of error codes to consider as normal """ decoded, code = popen( command, flag, @@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, env=env, shell=shell, decode=decode, ) - if code != 0: + if code not in expect: feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' feedback += f'returned: {decoded}\n' @@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which print the stdout and will return the error code of a command @@ -197,10 +213,24 @@ def chown(path, user, group): from pwd import getpwnam from grp import getgrnam - if os.path.exists(path): - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(path, uid, gid) + if user is None or group is None: + return False + + if not os.path.exists(path): + return False + + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(path, uid, gid) + return True + + +def chmod(path, bitmask): + if not os.path.exists(path): + return + if bitmask is None: + return + os.chmod(path, bitmask) def chmod_600(path): @@ -231,6 +261,13 @@ def chmod_755(path): os.chmod(path, bitmask) +def makedir(path, user=None, group=None): + if os.path.exists(path): + return + os.mkdir(path) + chown(path, user, group) + + def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries of colon-separated key-value pairs into a dict. diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index ce0e01308..d24a46220 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -98,11 +98,6 @@ def generate(relay): if not relay: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcp-relay/config.tmpl', relay) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index da01f16eb..1849ece0a 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -594,11 +594,6 @@ def generate(dhcp): if not dhcp or dhcp['disabled']: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index cb5a4bbfb..ecc739063 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -84,11 +84,6 @@ def generate(relay): if relay is None: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcpv6-relay/config.tmpl', relay) return None diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index ce98e39c3..159d16401 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -23,7 +23,7 @@ from copy import deepcopy from vyos.config import Config from vyos.template import render from vyos.util import call -from vyos.validate import is_subnet_connected +from vyos.validate import is_subnet_connected, is_ipv6 from vyos import ConfigError config_file = r'/run/dhcp-server/dhcpdv6.conf' @@ -37,24 +37,25 @@ default_config_data = { def get_config(): dhcpv6 = deepcopy(default_config_data) conf = Config() - if not conf.exists('service dhcpv6-server'): + base = ['service', 'dhcpv6-server'] + if not conf.exists(base): return None else: - conf.set_level('service dhcpv6-server') + conf.set_level(base) # Check for global disable of DHCPv6 service - if conf.exists('disable'): + if conf.exists(['disable']): dhcpv6['disabled'] = True return dhcpv6 # Preference of this DHCPv6 server compared with others - if conf.exists('preference'): - dhcpv6['preference'] = conf.return_value('preference') + if conf.exists(['preference']): + dhcpv6['preference'] = conf.return_value(['preference']) # check for multiple, shared networks served with DHCPv6 addresses - if conf.exists('shared-network-name'): - for network in conf.list_nodes('shared-network-name'): - conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network)) + if conf.exists(['shared-network-name']): + for network in conf.list_nodes(['shared-network-name']): + conf.set_level(base + ['shared-network-name', network]) config = { 'name': network, 'disabled': False, @@ -62,13 +63,13 @@ def get_config(): } # If disabled, the shared-network configuration becomes inactive - if conf.exists('disable'): + if conf.exists(['disable']): config['disabled'] = True # check for multiple subnet configurations in a shared network - if conf.exists('subnet'): - for net in conf.list_nodes('subnet'): - conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net)) + if conf.exists(['subnet']): + for net in conf.list_nodes(['subnet']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net]) subnet = { 'network': net, 'range6_prefix': [], @@ -94,25 +95,25 @@ def get_config(): # least one address range statement. The range statement gives the lowest and highest # IP addresses in a range. All IP addresses in the range should be in the subnet in # which the range statement is declared. - if conf.exists('address-range prefix'): - for prefix in conf.list_nodes('address-range prefix'): + if conf.exists(['address-range', 'prefix']): + for prefix in conf.list_nodes(['address-range', 'prefix']): range = { 'prefix': prefix, 'temporary': False } # Address range will be used for temporary addresses - if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])): + if conf.exists(['address-range' 'prefix', prefix, 'temporary']): range['temporary'] = True # Append to subnet temporary range6 list subnet['range6_prefix'].append(range) - if conf.exists('address-range start'): - for range in conf.list_nodes('address-range start'): + if conf.exists(['address-range', 'start']): + for range in conf.list_nodes(['address-range', 'start']): range = { 'start': range, - 'stop': conf.return_value('address-range start {0} stop'.format(range)) + 'stop': conf.return_value(['address-range', 'start', range, 'stop']) } # Append to subnet range6 list @@ -120,70 +121,68 @@ def get_config(): # The domain-search option specifies a 'search list' of Domain Names to be used # by the client to locate not-fully-qualified domain names. - if conf.exists('domain-search'): - for domain in conf.return_values('domain-search'): - subnet['domain_search'].append('"' + domain + '"') + if conf.exists(['domain-search']): + subnet['domain_search'] = conf.return_values(['domain-search']) # IPv6 address valid lifetime # (at the end the address is no longer usable by the client) # (set to 30 days, the usual IPv6 default) - if conf.exists('lease-time default'): - subnet['lease_def'] = conf.return_value('lease-time default') + if conf.exists(['lease-time', 'default']): + subnet['lease_def'] = conf.return_value(['lease-time', 'default']) # Time should be the maximum length in seconds that will be assigned to a lease. # The only exception to this is that Dynamic BOOTP lease lengths, which are not # specified by the client, are not limited by this maximum. - if conf.exists('lease-time maximum'): - subnet['lease_max'] = conf.return_value('lease-time maximum') + if conf.exists(['lease-time', 'maximum']): + subnet['lease_max'] = conf.return_value(['lease-time', 'maximum']) # Time should be the minimum length in seconds that will be assigned to a lease - if conf.exists('lease-time minimum'): - subnet['lease_min'] = conf.return_value('lease-time minimum') + if conf.exists(['lease-time', 'minimum']): + subnet['lease_min'] = conf.return_value(['lease-time', 'minimum']) # Specifies a list of Domain Name System name servers available to the client. # Servers should be listed in order of preference. - if conf.exists('name-server'): - subnet['dns_server'] = conf.return_values('name-server') + if conf.exists(['name-server']): + subnet['dns_server'] = conf.return_values(['name-server']) # Ancient NIS (Network Information Service) domain name - if conf.exists('nis-domain'): - subnet['nis_domain'] = conf.return_value('nis-domain') + if conf.exists(['nis-domain']): + subnet['nis_domain'] = conf.return_value(['nis-domain']) # Ancient NIS (Network Information Service) servers - if conf.exists('nis-server'): - subnet['nis_server'] = conf.return_values('nis-server') + if conf.exists(['nis-server']): + subnet['nis_server'] = conf.return_values(['nis-server']) # Ancient NIS+ (Network Information Service) domain name - if conf.exists('nisplus-domain'): - subnet['nisp_domain'] = conf.return_value('nisplus-domain') + if conf.exists(['nisplus-domain']): + subnet['nisp_domain'] = conf.return_value(['nisplus-domain']) # Ancient NIS+ (Network Information Service) servers - if conf.exists('nisplus-server'): - subnet['nisp_server'] = conf.return_values('nisplus-server') + if conf.exists(['nisplus-server']): + subnet['nisp_server'] = conf.return_values(['nisplus-server']) # Prefix Delegation (RFC 3633) - if conf.exists('prefix-delegation'): + if conf.exists(['prefix-delegation']): print('TODO: This option is actually not implemented right now!') # Local SIP server that is to be used for all outbound SIP requests - IPv6 address - if conf.exists('sip-server-address'): - subnet['sip_address'] = conf.return_values('sip-server-address') - - # Local SIP server that is to be used for all outbound SIP requests - hostname - if conf.exists('sip-server-name'): - for hostname in conf.return_values('sip-server-name'): - subnet['sip_hostname'].append('"' + hostname + '"') + if conf.exists(['sip-server']): + for value in conf.return_values(['sip-server']): + if is_ipv6(value): + subnet['sip_address'].append(value) + else: + subnet['sip_hostname'].append(value) # List of local SNTP servers available for the client to synchronize their clocks - if conf.exists('sntp-server'): - subnet['sntp_server'] = conf.return_values('sntp-server') + if conf.exists(['sntp-server']): + subnet['sntp_server'] = conf.return_values(['sntp-server']) # # Static DHCP v6 leases # - if conf.exists('static-mapping'): - for mapping in conf.list_nodes('static-mapping'): - conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + if conf.exists(['static-mapping']): + for mapping in conf.list_nodes(['static-mapping']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping]) mapping = { 'name': mapping, 'disabled': False, @@ -192,16 +191,16 @@ def get_config(): } # This static lease is disabled - if conf.exists('disable'): + if conf.exists(['disable']): mapping['disabled'] = True # IPv6 address used for this DHCP client - if conf.exists('ipv6-address'): - mapping['ipv6_address'] = conf.return_value('ipv6-address') + if conf.exists(['ipv6-address']): + mapping['ipv6_address'] = conf.return_value(['ipv6-address']) # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers - if conf.exists('identifier'): - mapping['client_identifier'] = conf.return_value('identifier') + if conf.exists(['identifier']): + mapping['client_identifier'] = conf.return_value(['identifier']) # append static mapping configuration tu subnet list subnet['static_mapping'].append(mapping) @@ -209,7 +208,6 @@ def get_config(): # append subnet configuration to shared network subnet list config['subnet'].append(subnet) - # append shared network configuration to config dictionary dhcpv6['shared_network'].append(config) @@ -335,11 +333,6 @@ def generate(dhcpv6): if not dhcpv6 or dhcpv6['disabled']: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6) return None diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 567dfa4b3..7f7417b00 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -152,10 +152,6 @@ def generate(dns): if dns is None: return None - dirname = os.path.dirname(config_file) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True) return None diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 038f77cf9..3386324ae 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -217,10 +217,6 @@ def generate(dyndns): if dyndns['deleted']: return None - dirname = os.path.dirname(config_file) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) # Config file must be accessible only by its owner diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index f942b7d2f..e72540f66 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -173,12 +173,6 @@ def generate(pppoe): config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, script_pppoe_ip_down, script_pppoe_ipv6_up] - # Ensure directories for config files exist - otherwise create them on demand - for file in config_files: - dirname = os.path.dirname(file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - # Always hang-up PPPoE connection prior generating new configuration file cmd(f'systemctl stop ppp@{intf}.service') @@ -189,27 +183,23 @@ def generate(pppoe): os.unlink(file) else: + # generated script must be executable + # Create PPP configuration files render(config_pppoe, 'pppoe/peer.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-pre-up.d render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-up.d render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-down.d render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ipv6-up.d render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', - pppoe, trim_blocks=True) - - # make generated script file executable - chmod_755(script_pppoe_pre_up) - chmod_755(script_pppoe_ip_up) - chmod_755(script_pppoe_ip_down) - chmod_755(script_pppoe_ipv6_up) + pppoe, trim_blocks=True, permission=0o755) return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 163778e22..a3a2a2648 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -152,12 +152,6 @@ def generate(wwan): config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, script_wwan_ip_up, script_wwan_ip_down] - # Ensure directories for config files exist - otherwise create them on demand - for file in config_files: - dirname = os.path.dirname(file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - # Always hang-up WWAN connection prior generating new configuration file cmd(f'systemctl stop ppp@{intf}.service') @@ -172,17 +166,18 @@ def generate(wwan): render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script render(config_wwan_chat, 'wwan/chat.tmpl', wwan) + + # generated script file must be executable + # Create script for ip-pre-up.d - render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan) + render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', + wwan, permission=0o755) # Create script for ip-up.d - render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan) + render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', + wwan, permission=0o755) # Create script for ip-down.d - render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan) - - # make generated script file executable - chmod_755(script_wwan_pre_up) - chmod_755(script_wwan_ip_up) - chmod_755(script_wwan_ip_down) + render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', + wwan, permission=0o755) return None diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index dffe7fcd4..8bc35bb45 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -79,14 +79,8 @@ def generate(salt): if not salt: return None - for file in [config_file, master_keyfile]: - dirname = os.path.dirname(file) - if not os.path.exists(dirname): - os.mkdir(dirname) - chown(dirname, salt['user'], salt['group']) - - render(config_file, 'salt-minion/minion.tmpl', salt) - chown(config_file, salt['user'], salt['group']) + render(config_file, 'salt-minion/minion.tmpl', salt, + user=salt['user'], group=salt['group']) if not os.path.exists(master_keyfile): if salt['master_key']: diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 17fa2c3f0..b53692d37 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -265,10 +265,6 @@ def generate(ipoe): if not ipoe: return None - dirname = os.path.dirname(ipoe_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True) if ipoe['auth_mode'] == 'local': diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 64890c992..e05b0ab2a 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -315,7 +315,7 @@ def get_config(): pppoe['mtu'] = conf.return_value(['mtu']) if conf.exists(['session-control']): - pppoe['session_control'] = conf.return_value(['session-control']) + pppoe['sesscrtl'] = conf.return_value(['session-control']) # ppp_options if conf.exists(['ppp-options']): @@ -429,10 +429,6 @@ def generate(pppoe): if not pppoe: return None - dirname = os.path.dirname(pppoe_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True) if pppoe['local_users']: diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index a4ef99d45..f312f2a17 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -340,10 +340,6 @@ def generate(l2tp): if not l2tp: return None - dirname = os.path.dirname(l2tp_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True) if l2tp['auth_mode'] == 'local': diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 046fc8f9c..085c9c2c6 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -247,10 +247,6 @@ def generate(pptp): if not pptp: return None - dirname = os.path.dirname(pptp_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True) if pptp['local_users']: diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index e6ce94709..d250cd3b0 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -303,10 +303,6 @@ def generate(sstp): if not sstp: return None - dirname = os.path.dirname(sstp_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - # accel-cmd reload doesn't work so any change results in a restart of the daemon render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index 59f92703c..f1167fcd2 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -15,8 +15,11 @@ function frr_alive () { # convert ip route command to vtysh function iptovtysh () { # prepare variables for vtysh command - VTYSH_DISTANCE="210" - VTYSH_TAG="210" + local VTYSH_DISTANCE="210" + local VTYSH_TAG="210" + local VTYSH_NETADDR="" + local VTYSH_GATEWAY="" + local VTYSH_DEV="" # convert default route to 0.0.0.0/0 if [ "$4" == "default" ] ; then VTYSH_NETADDR="0.0.0.0/0" @@ -74,3 +77,4 @@ function ip () { fi fi } + diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ce846f6c3..88a4d9db9 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -1,12 +1,74 @@ +# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state + if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then # delete dynamic nameservers from a configuration if lease was deleted logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" vyos-hostsd-client --delete-name-servers --tag dhcp-${interface} - # try to delete default ip route (NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state) + # try to delete default ip route for router in $old_routers; do logmsg info "Deleting default route: via $router dev ${interface}" ip -4 route del default via $router dev ${interface} done + # delete rfc3442 routes + if [ -n "$old_rfc3442_classless_static_routes" ]; then + set -- $old_rfc3442_classless_static_routes + while [ $# -gt 0 ]; do + net_length=$1 + via_arg='' + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + fi + # delete route (ip detects host routes automatically) + ip -4 route del "${net_address}/${net_length}" \ + ${via_arg} dev "${interface}" >/dev/null 2>&1 + done + fi fi if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 new file mode 100644 index 000000000..9202fe72d --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 @@ -0,0 +1,148 @@ +# support for RFC3442 routes in DHCP RENEW + +function convert_to_cidr () { + cidr="" + set -- $1 + while [ $# -gt 0 ]; do + net_length=$1 + + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + + cidr+="${net_address}/${net_length}:${gateway} " + done +} + +# main script starts here + +RUN="yes" + +if [ "$RUN" = "yes" ]; then + convert_to_cidr "$old_rfc3442_classless_static_routes" + old_cidr=$cidr + convert_to_cidr "$new_rfc3442_classless_static_routes" + new_cidr=$cidr + + if [ "$reason" = "RENEW" ]; then + if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then + logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes" + logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes" + if [ -z "$new_rfc3442_classless_static_routes" ]; then + # delete all routes from the old_rfc3442_classless_static_routes + for route in $old_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + elif [ -z "$old_rfc3442_classless_static_routes" ]; then + # add all routes from the new_rfc3442_classless_static_routes + for route in $new_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + else + # update routes + # delete old + for old_route in $old_cidr; do + match="false" + for new_route in $new_cidr; do + if [[ "$old_route" == "$new_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # delete old_route + network=$(printf "${old_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${old_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + # add new + for new_route in $new_cidr; do + match="false" + for old_route in $old_cidr; do + if [[ "$new_route" == "$old_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # add new_route + network=$(printf "${new_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${new_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + fi + fi + fi +fi diff --git a/src/migration-scripts/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1 new file mode 100755 index 000000000..6f1150da1 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/0-to-1 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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/>. + +# combine both sip-server-address and sip-server-name nodes to common sip-server + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'dhcpv6-server', 'shared-network-name'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + # we need to run this for every configured network + for network in config.list_nodes(base): + for subnet in config.list_nodes(base + [network, 'subnet']): + sip_server = [] + + # Do we have 'sip-server-address' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-address']) + + # Do we have 'sip-server-name' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-name']) + + # Write new CLI value for sip-server + for server in sip_server: + config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index b2fb5fe5a..69af427ec 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -71,8 +71,8 @@ def get_shutdown_status(): def check_shutdown(): output = get_shutdown_status() - dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') if output and 'MODE' in output: + dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') if output['MODE'] == 'reboot': print("Reboot is scheduled", utc2local(dt)) elif output['MODE'] == 'poweroff': diff --git a/src/systemd/dhclient6@.service b/src/systemd/dhclient6@.service new file mode 100644 index 000000000..fd69e4d48 --- /dev/null +++ b/src/systemd/dhclient6@.service @@ -0,0 +1,18 @@ +[Unit] +Description=DHCPv6 client on %i +Documentation=man:dhclient(8) +ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.conf +ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.options +After=vyos-router.service + +[Service] +WorkingDirectory=/var/lib/dhcp +Type=exec +EnvironmentFile=-/var/lib/dhcp/dhclient_v6_%i.options +PIDFile=/var/lib/dhcp/dhclient_v6_%i.pid +ExecStart=/sbin/dhclient -6 $DHCLIENT_OPTS +ExecStop=/sbin/dhclient -6 $DHCLIENT_OPTS -r +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service new file mode 100644 index 000000000..2ced1038a --- /dev/null +++ b/src/systemd/dhclient@.service @@ -0,0 +1,18 @@ +[Unit] +Description=DHCP client on %i +Documentation=man:dhclient(8) +ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf +ConditionPathExists=/var/lib/dhcp/dhclient_%i.options +After=vyos-router.service + +[Service] +WorkingDirectory=/var/lib/dhcp +Type=exec +EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options +PIDFile=/var/lib/dhcp/dhclient_%i.pid +ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS +ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service index 743f16840..27bebc57f 100644 --- a/src/systemd/isc-dhcp-server6.service +++ b/src/systemd/isc-dhcp-server6.service @@ -2,7 +2,7 @@ Description=ISC DHCP IPv6 server Documentation=man:dhcpd(8) RequiresMountsFor=/run -ConditionPathExists=/run/dhcp-server/dhcpd.conf +ConditionPathExists=/run/dhcp-server/dhcpdv6.conf After=vyos-router.service [Service] diff --git a/src/validators/numeric b/src/validators/numeric index 0a2d83d14..2cd5178b9 100755 --- a/src/validators/numeric +++ b/src/validators/numeric @@ -19,7 +19,6 @@ import sys import argparse -import re parser = argparse.ArgumentParser() parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values") @@ -50,8 +49,9 @@ if args.range: valid = False for r in args.range: try: - lower, upper = re.match(r'(\d+)\s*\-\s*(\d+)', r).groups() - lower, upper = int(lower), int(upper) + list = r.split('-') + lower = int(list[0]) + upper = int(list[1]) except: print("{0} is not a valid number range",format(args.range), file=sys.stderr) sys.exit(1) |