diff options
144 files changed, 3730 insertions, 4763 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d8177a5f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to VyOS + +You wan't to help us improve VyOS? This is awesome. We accept any kind of Pull +Requests on GitHub. To make the life of the maintainers and you as future +contributor (or maybe maintainer) much easier we have come up with some basic +rules. Instead of copy/pasting or maintaining two instances of how to contribute +to VyOS you can find the entire process documented in our online documentation: +https://docs.vyos.io/en/latest/contributing/development.html + +Also this guide might not be complete so any PR is much appreciated. + +It might also worth browsing our blog: https://blog.vyos.io @@ -88,12 +88,14 @@ op_mode_definitions: find $(CURDIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 # XXX: delete top level op mode node.def's that now live in other packages + rm -f $(OP_TMPL_DIR)/add/node.def rm -f $(OP_TMPL_DIR)/clear/node.def rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def rm -f $(OP_TMPL_DIR)/set/node.def rm -f $(OP_TMPL_DIR)/show/node.def rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/bgp/node.def rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def @@ -103,6 +105,10 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/delete/node.def rm -f $(OP_TMPL_DIR)/reset/vpn/node.def + # XXX: ping must be able to recursivly call itself as the + # options are provided from the script itself + ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ + .PHONY: component_versions .ONESHELL: component_versions: $(BUILD_DIR) $(obj) diff --git a/data/templates/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl index 84de5bf51..fca520efa 100644 --- a/data/templates/accel-ppp/ipoe.config.tmpl +++ b/data/templates/accel-ppp/ipoe.config.tmpl @@ -77,7 +77,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index b0ef17525..b9131684d 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -83,7 +83,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl index 370ca7946..5ad628fde 100644 --- a/data/templates/accel-ppp/pppoe.config.tmpl +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -93,7 +93,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/pptp.config.tmpl b/data/templates/accel-ppp/pptp.config.tmpl index 0bbfc13c5..e0f2c6da9 100644 --- a/data/templates/accel-ppp/pptp.config.tmpl +++ b/data/templates/accel-ppp/pptp.config.tmpl @@ -66,7 +66,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index 2c8c00023..c9e4a1d7d 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -69,7 +69,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl index 3d8c3fe94..d0c7d8bf9 100644 --- a/data/templates/bcast-relay/udp-broadcast-relay.tmpl +++ b/data/templates/bcast-relay/udp-broadcast-relay.tmpl @@ -4,4 +4,4 @@ {%- if description %} # Comment: {{ description }} {% endif %} -DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" +DAEMON_ARGS="{{ '-s ' + address if address is defined }} {{ instance }} {{ port }} {{ interface | join(' ') }}" diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl index 12786b777..a0ba2c9ef 100644 --- a/data/templates/dhcp-client/daemon-options.tmpl +++ b/data/templates/dhcp-client/daemon-options.tmpl @@ -1 +1 @@ -DHCLIENT_OPTS="-nw -cf {{ conf_file }} -pf {{ pid_file }} -lf {{ lease_file }} {{ '-S' if dhcpv6_prm_only }} {{ '-T' if dhcpv6_temporary }} {{ ifname }}" +DHCLIENT_OPTS="-nw -cf /var/lib/dhcp/dhclient_{{ifname}}.conf -pf /var/lib/dhcp/dhclient_{{ifname}}.pid -lf /var/lib/dhcp/dhclient_{{ifname}}.leases {{ifname}}" diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index ab772b5f6..fe2a67f08 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -4,14 +4,14 @@ 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; + send host-name "{{ dhcp_options.host_name }}"; +{% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %} + send dhcp-client-identifier "{{ dhcp_options.client_id }}"; +{% endif %} +{% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %} + send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}"; +{% endif %} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; } diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl index 490f14726..112431c5f 100644 --- a/data/templates/dhcp-client/ipv6.tmpl +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -4,41 +4,44 @@ interface {{ ifname }} { request domain-name-servers; request domain-name; -{% if dhcpv6_prm_only %} +{% if dhcpv6_options is defined %} +{% if dhcpv6_options.parameters_only is defined %} information-only; -{% endif %} -{% if not dhcpv6_temporary %} +{% endif %} +{% if dhcpv6_options.temporary is not defined %} send ia-na 1; # non-temporary address -{% endif %} -{% if dhcpv6_pd_interfaces %} +{% endif %} +{% if dhcpv6_options.prefix_delegation is defined %} send ia-pd 2; # prefix delegation +{% endif %} {% endif %} }; -{% if not dhcpv6_temporary %} +{% if dhcpv6_options is defined %} +{% if dhcpv6_options.temporary is not defined %} id-assoc na 1 { # Identity association NA }; -{% endif %} +{% endif %} -{% if dhcpv6_pd_interfaces %} +{% if dhcpv6_options.prefix_delegation is defined %} id-assoc pd 2 { -{% if dhcpv6_pd_length %} - prefix ::/{{ dhcpv6_pd_length }} infinity; -{% endif %} -{% for intf in dhcpv6_pd_interfaces %} - prefix-interface {{ intf.ifname }} { -{% if intf.sla_id %} - sla-id {{ intf.sla_id }}; -{% endif %} -{% if intf.sla_len %} - sla-len {{ intf.sla_len }}; -{% endif %} -{% if intf.if_id %} - ifid {{ intf.if_id }}; -{% endif %} +{% if dhcpv6_options.prefix_delegation.length is defined %} + prefix ::/{{ dhcpv6_options.prefix_delegation.length }} infinity; +{% endif %} +{% for interface in dhcpv6_options.prefix_delegation.interface %} + prefix-interface {{ interface }} { +{% if dhcpv6_options.prefix_delegation.interface[interface].sla_id is defined %} + sla-id {{ dhcpv6_options.prefix_delegation.interface[interface].sla_id }}; +{% endif %} +{% if dhcpv6_options.prefix_delegation.interface[interface].sla_len is defined %} + sla-len {{ dhcpv6_options.prefix_delegation.interface[interface].sla_len }}; +{% endif %} +{% if dhcpv6_options.prefix_delegation.interface[interface].address is defined %} + ifid {{ dhcpv6_options.prefix_delegation.interface[interface].address }}; +{% endif %} }; -{% endfor %} +{% endfor %} }; -{% endif %} - +{% endif %} +{% endif %} diff --git a/data/templates/dhcp-client/ipv6_new.tmpl b/data/templates/dhcp-client/ipv6_new.tmpl deleted file mode 100644 index 112431c5f..000000000 --- a/data/templates/dhcp-client/ipv6_new.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -# generated by dhcp.py -# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/ - -interface {{ ifname }} { - request domain-name-servers; - request domain-name; -{% if dhcpv6_options is defined %} -{% if dhcpv6_options.parameters_only is defined %} - information-only; -{% endif %} -{% if dhcpv6_options.temporary is not defined %} - send ia-na 1; # non-temporary address -{% endif %} -{% if dhcpv6_options.prefix_delegation is defined %} - send ia-pd 2; # prefix delegation -{% endif %} -{% endif %} -}; - -{% if dhcpv6_options is defined %} -{% if dhcpv6_options.temporary is not defined %} -id-assoc na 1 { - # Identity association NA -}; -{% endif %} - -{% if dhcpv6_options.prefix_delegation is defined %} -id-assoc pd 2 { -{% if dhcpv6_options.prefix_delegation.length is defined %} - prefix ::/{{ dhcpv6_options.prefix_delegation.length }} infinity; -{% endif %} -{% for interface in dhcpv6_options.prefix_delegation.interface %} - prefix-interface {{ interface }} { -{% if dhcpv6_options.prefix_delegation.interface[interface].sla_id is defined %} - sla-id {{ dhcpv6_options.prefix_delegation.interface[interface].sla_id }}; -{% endif %} -{% if dhcpv6_options.prefix_delegation.interface[interface].sla_len is defined %} - sla-len {{ dhcpv6_options.prefix_delegation.interface[interface].sla_len }}; -{% endif %} -{% if dhcpv6_options.prefix_delegation.interface[interface].address is defined %} - ifid {{ dhcpv6_options.prefix_delegation.interface[interface].address }}; -{% endif %} - }; -{% endfor %} -}; -{% endif %} -{% endif %} diff --git a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl index 6d1760199..b0d99d9ae 100644 --- a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl +++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl @@ -17,7 +17,7 @@ addNTA("{{ a }}.", "{{ tag }} alias") -- from 'service dns forwarding domain' {%- for zone, zonedata in forward_zones.items() %} {%- if zonedata['addNTA'] %} -addNTA("{{ zone }}.", "static") +addNTA("{{ zone }}", "static") {%- endif %} {%- endfor %} {%- endif %} diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 8108d5e0f..0c29f536b 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -6,7 +6,7 @@ flush table nat {% if helper_functions == 'remove' %} {# NAT if going to be disabled - remove rules and targets from nftables #} -{% set base_command = "delete rule ip raw" %} +{% set base_command = "delete rule ip raw" %} {{ base_command }} PREROUTING handle {{ pre_ct_ignore }} {{ base_command }} OUTPUT handle {{ out_ct_ignore }} {{ base_command }} PREROUTING handle {{ pre_ct_conntrack }} @@ -19,7 +19,7 @@ delete chain ip raw NAT_CONNTRACK add chain ip raw NAT_CONNTRACK add rule ip raw NAT_CONNTRACK counter accept -{% set base_command = "add rule ip raw" %} +{% set base_command = "add rule ip raw" %} {{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER {{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER @@ -48,10 +48,11 @@ add rule ip raw NAT_CONNTRACK counter accept {% set comment = "DST-NAT-" + rule.number %} {% if chain == "PREROUTING" %} -{% set interface = " iifname \"" + rule.interface_in + "\"" %} +{% set interface = " iifname \"" + rule.interface_in + "\"" if rule.interface_in is defined and rule.interface_in != 'any' else '' %} {% set trns_addr = "dnat to " + rule.translation_address %} + {% elif chain == "POSTROUTING" %} -{% set interface = " oifname \"" + rule.interface_out + "\"" %} +{% set interface = " oifname \"" + rule.interface_out + "\"" if rule.interface_out is defined and rule.interface_out != 'any' else '' %} {% if rule.translation_address == 'masquerade' %} {% set trns_addr = rule.translation_address %} {% if rule.translation_port %} diff --git a/data/templates/frr/bgp.frr.tmpl b/data/templates/frr/bgp.frr.tmpl index cd6f31c93..cdf4cb4fe 100644 --- a/data/templates/frr/bgp.frr.tmpl +++ b/data/templates/frr/bgp.frr.tmpl @@ -1 +1 @@ -!
+! diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl index 3fc4db67e..80f4ab047 100644 --- a/data/templates/mdns-repeater/mdns-repeater.tmpl +++ b/data/templates/mdns-repeater/mdns-repeater.tmpl @@ -1,2 +1,2 @@ ### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interfaces | join(' ') }}" +DAEMON_ARGS="{{ interface | join(' ') }}" diff --git a/data/templates/ntp/ntp.conf.tmpl b/data/templates/ntp/ntp.conf.tmpl index 1c51929fd..6ef0c0f2c 100644 --- a/data/templates/ntp/ntp.conf.tmpl +++ b/data/templates/ntp/ntp.conf.tmpl @@ -19,7 +19,7 @@ restrict -6 ::1 {% set options = options + 'noselect ' if server[srv].noselect is defined else '' %} {% set options = options + 'preempt ' if server[srv].preempt is defined else '' %} {% set options = options + 'prefer ' if server[srv].prefer is defined else '' %} -server {{ srv }} iburst {{ options }} +server {{ srv | replace('_', '-') }} iburst {{ options }} {% endfor %} {% endif %} diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl index f69f9fc1a..7b1952a80 100644 --- a/data/templates/pppoe/ip-down.script.tmpl +++ b/data/templates/pppoe/ip-down.script.tmpl @@ -10,7 +10,7 @@ fi DIALER_PID=$(cat /var/run/{{ ifname }}.pid) logger -t pppd[$DIALER_PID] "executing $0" -{% if not on_demand %} +{% if connect_on_demand is not defined %} # See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a # VRF, this is needed to properly insert the default route. VRF_NAME="" @@ -30,7 +30,7 @@ vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}" {% endif %} {% endif %} -{% if dhcpv6_pd_interfaces %} -# Start wide dhcpv6 client +{% if dhcpv6_options is defined and dhcpv6_options.prefix_delegation is defined %} +# Stop wide dhcpv6 client systemctl stop dhcp6c@{{ ifname }}.service {% endif %} diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl index 6adea4bd7..568e21c4e 100644 --- a/data/templates/pppoe/ip-up.script.tmpl +++ b/data/templates/pppoe/ip-up.script.tmpl @@ -6,7 +6,7 @@ if [ "$6" != "{{ ifname }}" ]; then exit fi -{% if not on_demand %} +{% if connect_on_demand is not defined %} # add some info to syslog DIALER_PID=$(cat /var/run/{{ ifname }}.pid) logger -t pppd[$DIALER_PID] "executing $0" diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl index 073623eac..cebfc54b5 100644 --- a/data/templates/router-advert/radvd.conf.tmpl +++ b/data/templates/router-advert/radvd.conf.tmpl @@ -1,37 +1,47 @@ ### Autogenerated by service_router-advert.py ### -{% for i in interfaces -%} -interface {{ i.name }} { +{% if interface is defined and interface is not none %} +{% for iface in interface %} +interface {{ iface }} { IgnoreIfMissing on; - AdvDefaultPreference {{ i.default_preference }}; - AdvManagedFlag {{ i.managed_flag }}; - MaxRtrAdvInterval {{ i.interval_max }}; -{% if i.interval_min %} - MinRtrAdvInterval {{ i.interval_min }}; -{% endif %} - AdvReachableTime {{ i.reachable_time }}; - AdvIntervalOpt {{ i.send_advert }}; - AdvSendAdvert {{ i.send_advert }}; -{% if i.default_lifetime %} - AdvDefaultLifetime {{ i.default_lifetime }}; -{% endif %} -{% if i.link_mtu %} - AdvLinkMTU {{ i.link_mtu }}; -{% endif %} - AdvOtherConfigFlag {{ i.other_config_flag }}; - AdvRetransTimer {{ i.retrans_timer }}; - AdvCurHopLimit {{ i.hop_limit }}; -{% for p in i.prefixes %} - prefix {{ p.prefix }} { - AdvAutonomous {{ p.autonomous_flag }}; - AdvValidLifetime {{ p.valid_lifetime }}; - AdvOnLink {{ p.on_link }}; - AdvPreferredLifetime {{ p.preferred_lifetime }}; +{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %} + AdvDefaultPreference {{ interface[iface].default_preference }}; +{% endif %} +{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %} + AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }}; +{% endif %} +{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %} + MaxRtrAdvInterval {{ interface[iface].interval.max }}; +{% endif %} +{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %} + MinRtrAdvInterval {{ interface[iface].interval.min }}; +{% endif %} +{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %} + AdvReachableTime {{ interface[iface].reachable_time }}; +{% endif %} + AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; + AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; +{% if interface[iface].default_lifetime is defined %} + AdvDefaultLifetime {{ interface[iface].default_lifetime }}; +{% endif %} +{% if interface[iface].link_mtu is defined %} + AdvLinkMTU {{ interface[iface].link_mtu }}; +{% endif %} + AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }}; + AdvRetransTimer {{ interface[iface].retrans_timer }}; + AdvCurHopLimit {{ interface[iface].hop_limit }}; +{% for prefix in interface[iface].prefix %} + prefix {{ prefix }} { + AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }}; + AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }}; + AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }}; + AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }}; }; -{% endfor %} -{% if i.name_server %} - RDNSS {{ i.name_server | join(" ") }} { +{% endfor %} +{% if interface[iface].name_server is defined %} + RDNSS {{ interface[iface].name_server | join(" ") }} { }; -{% endif %} +{% endif %} }; -{% endfor -%} +{% endfor -%} +{% endif %} diff --git a/data/templates/snmp/etc.snmp.conf.tmpl b/data/templates/snmp/etc.snmp.conf.tmpl index 159578906..6e4c6f063 100644 --- a/data/templates/snmp/etc.snmp.conf.tmpl +++ b/data/templates/snmp/etc.snmp.conf.tmpl @@ -1,4 +1,4 @@ ### Autogenerated by snmp.py ### -{% if trap_source -%} +{% if trap_source %} clientaddr {{ trap_source }} {% endif %} diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl index 1659abf93..278506350 100644 --- a/data/templates/snmp/etc.snmpd.conf.tmpl +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -32,87 +32,84 @@ sysDescr VyOS {{ version }} {% if description %} # Description SysDescr {{ description }} -{%- endif %} +{% endif %} # Listen agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} # SNMP communities -{%- for c in communities %} - -{%- if c.network_v4 %} -{%- for network in c.network_v4 %} +{% for c in communities %} +{% if c.network_v4 %} +{% for network in c.network_v4 %} {{ c.authorization }}community {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} +{% endfor %} +{% elif not c.has_source %} {{ c.authorization }}community {{ c.name }} -{%- endif %} - -{%- if c.network_v6 %} -{%- for network in c.network_v6 %} +{% endif %} +{% if c.network_v6 %} +{% for network in c.network_v6 %} {{ c.authorization }}community6 {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} +{% endfor %} +{% elif not c.has_source %} {{ c.authorization }}community6 {{ c.name }} -{%- endif %} - -{%- endfor %} +{% endif %} +{% endfor %} {% if contact %} # system contact information SysContact {{ contact }} -{%- endif %} +{% endif %} {% if location %} # system location information SysLocation {{ location }} -{%- endif %} +{% endif %} -{% if smux_peers -%} +{% if smux_peers %} # additional smux peers -{%- for sp in smux_peers %} +{% for sp in smux_peers %} smuxpeer {{ sp }} -{%- endfor %} -{%- endif %} +{% endfor %} +{% endif %} -{% if trap_targets -%} +{% if trap_targets %} # if there is a problem - tell someone! -{%- for t in trap_targets %} -trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} -{%- endfor %} -{%- endif %} +{% for trap in trap_targets %} +trap2sink {{ trap.target }}{{ ":" + trap.port if trap.port is defined }} {{ trap.community }} +{% endfor %} +{% endif %} -{%- if v3_enabled %} +{% if v3_enabled %} # # SNMPv3 stuff goes here # # views -{%- for v in v3_views %} -{%- for oid in v.oids %} -view {{ v.name }} included .{{ oid.oid }} -{%- endfor %} -{%- endfor %} +{% for view in v3_views %} +{% for oid in view.oids %} +view {{ view.name }} included .{{ oid.oid }} +{% endfor %} +{% endfor %} # access # context sec.model sec.level match read write notif -{%- for g in v3_groups %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none -{%- endfor %} +{% for group in v3_groups %} +access {{ group.name }} "" usm {{ group.seclevel }} exact {{ group.view }} {% if group.mode == 'ro' %}none{% else %}{{ group.view }}{% endif %} none +{% endfor %} # trap-target -{%- for t in v3_traps %} +{% for t in v3_traps %} trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{%- endfor %} +{% endfor %} # group -{%- for u in v3_users %} +{% for u in v3_users %} group {{ u.group }} usm {{ u.name }} -{% endfor %} -{%- endif %} +{% endfor %} +{% endif %} {% if script_ext %} # extension scripts -{%- for ext in script_ext|sort(attribute='name') %} +{% for ext in script_ext|sort(attribute='name') %} extend {{ ext.name }} {{ ext.script }} -{%- endfor %} +{% endfor %} {% endif %} diff --git a/data/templates/snmp/var.snmpd.conf.tmpl b/data/templates/snmp/var.snmpd.conf.tmpl index 0b8e9f291..6cbc687ef 100644 --- a/data/templates/snmp/var.snmpd.conf.tmpl +++ b/data/templates/snmp/var.snmpd.conf.tmpl @@ -3,14 +3,12 @@ {%- for u in v3_users %} {%- if u.authOID == 'none' %} createUser {{ u.name }} -{%- elif u.authPassword %} -createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} {%- else %} -usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x +usmUser 1 3 0x{{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} 0x{{ u.authMasterKey }} {{ u.privOID }} 0x{{ u.privMasterKey }} 0x {%- endif %} {%- endfor %} createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES {%- if v3_engineid %} -oldEngineID {{ v3_engineid }} +oldEngineID 0x{{ v3_engineid }} {%- endif %} diff --git a/data/templates/ssh/override.conf.tmpl b/data/templates/ssh/override.conf.tmpl index 4276366ae..843aa927b 100644 --- a/data/templates/ssh/override.conf.tmpl +++ b/data/templates/ssh/override.conf.tmpl @@ -2,9 +2,10 @@ [Unit] StartLimitIntervalSec=0 After=vyos-router.service +ConditionPathExists={{config_file}} [Service] ExecStart= -ExecStart={{vrf_command}}/usr/sbin/sshd -D $SSHD_OPTS +ExecStart={{vrf_command}}/usr/sbin/sshd -f {{config_file}} -D $SSHD_OPTS RestartSec=10 diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl index dbc5c3646..4fde24255 100644 --- a/data/templates/ssh/sshd_config.tmpl +++ b/data/templates/ssh/sshd_config.tmpl @@ -46,7 +46,7 @@ Port {{ value }} {% endif %} # Gives the verbosity level that is used when logging messages from sshd -LogLevel {{ loglevel }} +LogLevel {{ loglevel | upper }} # Specifies whether password authentication is allowed PasswordAuthentication {{ "no" if disable_password_authentication is defined else "yes" }} diff --git a/data/templates/system/curlrc.tmpl b/data/templates/system/curlrc.tmpl new file mode 100644 index 000000000..3e5ce801c --- /dev/null +++ b/data/templates/system/curlrc.tmpl @@ -0,0 +1,8 @@ +{% if http_client is defined %} +{% if http_client.source_interface is defined %} +--interface "{{ http_client.source_interface }}" +{% endif %} +{% if http_client.source_address is defined %} +--interface "{{ http_client.source_address }}" +{% endif %} +{% endif %} diff --git a/data/templates/system/ssh_config.tmpl b/data/templates/system/ssh_config.tmpl new file mode 100644 index 000000000..509bd5479 --- /dev/null +++ b/data/templates/system/ssh_config.tmpl @@ -0,0 +1,3 @@ +{% if ssh_client is defined and ssh_client.source_address is defined and ssh_client.source_address is not none %}
+BindAddress {{ ssh_client.source_address }}
+{% endif %}
diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl index b21bacc1e..91df57aab 100644 --- a/data/templates/wifi/cfg80211.conf.tmpl +++ b/data/templates/wifi/cfg80211.conf.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -options cfg80211 ieee80211_regdom={{ regdom }} -{% endif %} +{{ 'options cfg80211 ieee80211_regdom=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl index 750ad86ee..6cd125e37 100644 --- a/data/templates/wifi/crda.tmpl +++ b/data/templates/wifi/crda.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -REGDOMAIN={{ regdom }} -{% endif %} +{{ 'REGDOMAIN=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index d6068e4db..765668c57 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -9,7 +9,7 @@ device_name={{ description | truncate(32, True) }} # management frames with the Host AP driver); wlan0 with many nl80211 drivers # Note: This attribute can be overridden by the values supplied with the '-i' # command line parameter. -interface={{ intf }} +interface={{ ifname }} # Driver interface type (hostap/wired/none/nl80211/bsd); # default: hostap). nl80211 is used with all Linux mac80211 drivers. @@ -28,8 +28,7 @@ logger_syslog_level=0 logger_stdout=-1 logger_stdout_level=0 -{%- if country_code %} - +{% if country_code %} # Country code (ISO/IEC 3166-1). Used to set regulatory domain. # Set as needed to indicate country in which device is operating. # This can limit available channels and transmit power. @@ -42,14 +41,12 @@ country_code={{ country_code }} ieee80211d=1 {% endif %} -{%- if ssid %} - +{% if ssid %} # SSID to be used in IEEE 802.11 management frames ssid={{ ssid }} {% endif %} -{%- if channel %} - +{% if channel %} # Channel number (IEEE 802.11) # (default: 0, i.e., not set) # Please note that some drivers do not use this value from hostapd and the @@ -61,8 +58,7 @@ ssid={{ ssid }} channel={{ channel }} {% endif %} -{%- if mode %} - +{% if mode %} # Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), # g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used # with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this @@ -71,29 +67,30 @@ channel={{ channel }} # special value "any" can be used to indicate that any support band can be used. # This special case is currently supported only with drivers with which # offloaded ACS is used. -{% if 'n' in mode -%} +{% if 'n' in mode %} hw_mode=g -{% elif 'ac' in mode -%} +{% elif 'ac' in mode %} hw_mode=a ieee80211h=1 ieee80211ac=1 -{% else -%} +{% else %} hw_mode={{ mode }} -{% endif %} +{% endif %} {% endif %} # ieee80211w: Whether management frame protection (MFP) is enabled # 0 = disabled (default) # 1 = optional # 2 = required -{% if 'disabled' in mgmt_frame_protection -%} +{% if 'disabled' in mgmt_frame_protection %} ieee80211w=0 -{% elif 'optional' in mgmt_frame_protection -%} +{% elif 'optional' in mgmt_frame_protection %} ieee80211w=1 -{% elif 'required' in mgmt_frame_protection -%} +{% elif 'required' in mgmt_frame_protection %} ieee80211w=2 {% endif %} +{% if capabilities is defined and capabilities.ht is defined %} # ht_capab: HT capabilities (list of flags) # LDPC coding capability: [LDPC] = supported # Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary @@ -127,79 +124,50 @@ ieee80211w=2 # DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) # 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) # L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% if cap_ht %} -ht_capab= -{%- endif -%} - -{%- if cap_ht_40mhz_incapable -%} -[40-INTOLERANT] -{%- endif -%} - -{%- if cap_ht_delayed_block_ack -%} -[DELAYED-BA] -{%- endif -%} - -{%- if cap_ht_dsss_cck_40 -%} -[DSSS_CCK-40] -{%- endif -%} - -{%- if cap_ht_greenfield -%} -[GF] -{%- endif -%} - -{%- if cap_ht_ldpc -%} -[LDPC] -{%- endif -%} - -{%- if cap_ht_lsig_protection -%} -[LSIG-TXOP-PROT] -{%- endif -%} - -{%- if cap_ht_max_amsdu -%} -[MAX-AMSDU-{{ cap_ht_max_amsdu }}] -{%- endif -%} - -{%- if cap_ht_smps -%} -[SMPS-{{ cap_ht_smps | upper }}] -{%- endif -%} - -{%- if cap_ht_chan_set_width -%} -{%- for csw in cap_ht_chan_set_width -%} -[{{ csw | upper }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_short_gi -%} -{%- for gi in cap_ht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_stbc_tx -%} -[TX-STBC] -{%- endif -%} -{%- if cap_ht_stbc_rx -%} -[RX-STBC{{ cap_ht_stbc_rx }}] -{%- endif %} +{% set output = '' %} +{% set output = output + '[40-INTOLERANT]' if capabilities.ht.fourtymhz_incapable is defined else '' %} +{% set output = output + '[DELAYED-BA]' if capabilities.ht.delayed_block_ack is defined else '' %} +{% set output = output + '[DSSS_CCK-40]' if capabilities.ht.dsss_cck_40 is defined else '' %} +{% set output = output + '[GF]' if capabilities.ht.greenfield is defined else '' %} +{% set output = output + '[LDPC]' if capabilities.ht.ldpc is defined else '' %} +{% set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection is defined else '' %} +{% set output = output + '[TX-STBC]' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' if capabilities.ht.max_amsdu is defined else '' %} +{% set output = output + '[SMPS-' + capabilities.ht.smps | upper + ']' if capabilities.ht.smps is defined else '' %} + +{% if capabilities.ht.channel_set_width is defined %} +{% for csw in capabilities.ht.channel_set_width %} +{% set output = output + '[' + csw | upper + ']' %} +{% endfor %} +{% endif %} -# Required for full HT and VHT functionality -wme_enabled=1 +{% if capabilities.ht.short_gi is defined %} +{% for short_gi in capabilities.ht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} -{% if cap_ht_powersave -%} +ht_capab={{ output }} + +{% if capabilities.ht.auto_powersave is defined %} # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] # Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) uapsd_advertisement_enabled=1 -{%- endif %} +{% endif %} + +{% endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 -{% if cap_req_ht -%} + +{% if capabilities is defined and capabilities.require_ht is defined %} # Require stations to support HT PHY (reject association if they do not) require_ht=1 {% endif %} -{%- if cap_vht_chan_set_width -%} -vht_oper_chwidth={{ cap_vht_chan_set_width }} -{%- endif %} - +{% if capabilities is defined and capabilities.vht is defined %} # vht_capab: VHT capabilities (list of flags) # # vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] @@ -316,133 +284,95 @@ vht_oper_chwidth={{ cap_vht_chan_set_width }} # Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] # Indicates the possibility of Tx antenna pattern change # 0 = Tx antenna pattern might change during the lifetime of an association -# 1 = Tx antenna pattern does not change during the lifetime of an association -{% if cap_vht %} -vht_capab= -{%- endif -%} - -{%- if cap_vht_max_mpdu -%} -[MAX-MPDU-{{ cap_vht_max_mpdu }}] -{%- endif -%} - -{%- if cap_vht_max_mpdu_exp -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -{%- if '2' in cap_vht_chan_set_width -%} -[VHT160] -{%- elif '3' in cap_vht_chan_set_width -%} -[VHT160-80PLUS80] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_stbc_tx -%} -[TX-STBC-2BY1] -{%- endif -%} - -{%- if cap_vht_stbc_rx -%} -[RX-STBC-{{ cap_vht_stbc_rx }}] -{%- endif -%} - -{%- if cap_vht_link_adaptation -%} -{%- if 'unsolicited' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT2] -{%- elif 'both' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT3] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_short_gi -%} -{%- for gi in cap_vht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_ldpc -%} -[RXLDPC] -{%- endif -%} - -{%- if cap_vht_tx_powersave -%} -[VHT-TXOP-PS] -{%- endif -%} - -{%- if cap_vht_vht_cf -%} -[HTC-VHT] -{%- endif -%} - -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[SU-BEAMFORMER] -{%- elif 'single-user-beamformee' in beamform -%} -[SU-BEAMFORMEE] -{%- elif 'multi-user-beamformer' in beamform -%} -[MU-BEAMFORMER] -{%- elif 'multi-user-beamformee' in beamform -%} -[MU-BEAMFORMEE] -{%- endif -%} -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_antenna_fixed -%} -[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] -{%- endif -%} - -{%- if cap_vht_antenna_cnt -%} -{%- if cap_vht_antenna_cnt|int > 1 -%} -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -{%- if cap_vht_antenna_cnt|int < 6 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] -{%- endif -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endfor -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endif -%} -{%- endif %} +# 1 = Tx antenna pattern does not change during the lifetime of an + +{% if capabilities.vht.center_channel_freq.freq_1 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ capabilities.vht.center_channel_freq.freq_1 }} +{% endif %} + +{% if capabilities.vht.center_channel_freq.freq_2 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ capabilities.vht.center_channel_freq.freq_2 }} +{% endif %} + +{% if capabilities.vht.channel_set_width is defined %} +vht_oper_chwidth={{ capabilities.vht.channel_set_width }} +{% endif %} + +{% set output = '' %} +{% set output = output + '[TX-STBC-2BY1]' if capabilities.vht.stbc.tx is defined else '' %} +{% set output = output + '[RXLDPC]' if capabilities.vht.ldpc is defined else '' %} +{% set output = output + '[VHT-TXOP-PS]' if capabilities.vht.tx_powersave is defined else '' %} +{% set output = output + '[HTC-VHT]' if capabilities.vht.vht_cf is defined else '' %} +{% set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} +{% set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} + +{% set output = output + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' if capabilities.vht.stbc.rx is defined else '' %} +{% set output = output + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' if capabilities.vht.max_mpdu is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} + +{% set output = output + '[VHT160]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' else '' %} +{% set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' else '' %} +{% set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %} +{% set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' else '' %} + +{% if capabilities.vht.short_gi is defined %} +{% for short_gi in capabilities.vht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.beamform %} +{% for beamform in capabilities.vht.beamform %} +{% set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} +{% set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} +{% set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %} +{% set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 %} +{% if capabilities.vht.beamform %} +{% if beamform == 'single-user-beamformer' %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% endif %} +{% endif %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} +{% endif %} +{% endif %} +{% endif %} + +vht_capab={{ output }} +{% endif %} # ieee80211n: Whether IEEE 802.11n (HT) is enabled # 0 = disabled (default) # 1 = enabled # Note: You will also need to enable WMM for full HT functionality. # Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. -{% if cap_req_vht -%} +{% if capabilities is defined and capabilities.require_vht is defined %} ieee80211n=0 # Require stations to support VHT PHY (reject association if they do not) require_vht=1 -{% else -%} -{% if 'n' in mode or 'ac' in mode -%} +{% else %} +{% if 'n' in mode or 'ac' in mode %} ieee80211n=1 -{% else -%} +{% else %} ieee80211n=0 -{%- endif %} +{% endif %} {% endif %} -{% if cap_vht_center_freq_1 -%} -# center freq = 5 GHz + (5 * index) -# So index 42 gives center freq 5.210 GHz -# which is channel 42 in 5G band -vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} -{% endif %} - -{% if cap_vht_center_freq_2 -%} -# center freq = 5 GHz + (5 * index) -# So index 159 gives center freq 5.795 GHz -# which is channel 159 in 5G band -vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} -{% endif %} - -{% if disable_broadcast_ssid -%} +{% if disable_broadcast_ssid is defined %} # Send empty SSID in beacons and ignore probe request frames that do not # specify full SSID, i.e., require stations to know SSID. # default: disabled (0) @@ -463,7 +393,7 @@ ignore_broadcast_ssid=1 # 2 = use external RADIUS server (accept/deny lists are searched first) macaddr_acl=0 -{% if max_stations -%} +{% if max_stations is defined %} # Maximum number of stations allowed in station table. New stations will be # rejected after the station table is full. IEEE 802.11 has a limit of 2007 # different association IDs, so this number should not be larger than that. @@ -471,13 +401,13 @@ macaddr_acl=0 max_num_sta={{ max_stations }} {% endif %} -{% if isolate_stations -%} +{% if isolate_stations is defined %} # Client isolation can be used to prevent low-level bridging of frames between # associated stations in the BSS. By default, this bridging is allowed. ap_isolate=1 {% endif %} -{% if reduce_transmit_power -%} +{% if reduce_transmit_power is defined %} # Add Power Constraint element to Beacon and Probe Response frames # This config option adds Power Constraint element when applicable and Country # element is added. Power Constraint element is required by Transmit Power @@ -486,14 +416,15 @@ ap_isolate=1 local_pwr_constraint={{ reduce_transmit_power }} {% endif %} -{% if expunge_failing_stations -%} +{% if expunge_failing_stations is defined %} # Disassociate stations based on excessive transmission failures or other # indications of connection loss. This depends on the driver capabilities and # may not be available with all drivers. disassoc_low_ack=1 {% endif %} -{% if sec_wep -%} + +{% if security is defined and security.wep is defined %} # IEEE 802.11 specifies two authentication algorithms. hostapd can be # configured to allow both of these or only one. Open system authentication # should be used with IEEE 802.1X. @@ -522,13 +453,14 @@ wep_default_key=0 # digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or # 128-bit (152-bit) WEP is used. # Only the default key must be supplied; the others are optional. -{% if sec_wep_key -%} -{% for key in sec_wep_key -%} -wep_key{{ loop.index -1 }}={{ key}} -{% endfor %} -{%- endif %} +{% if security.wep.key is defined %} +{% for key in sec_wep_key %} +wep_key{{ loop.index -1 }}={{ security.wep.key }} +{% endfor %} +{% endif %} -{% elif sec_wpa -%} + +{% elif security is defined and security.wpa is defined %} ##### WPA/IEEE 802.11i configuration ########################################## # Enable WPA. Setting this variable configures the AP to require WPA (either @@ -542,15 +474,17 @@ wep_key{{ loop.index -1 }}={{ key}} # and/or WPA2 (full IEEE 802.11i/RSN): # bit0 = WPA # bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) -{% if 'both' in sec_wpa_mode -%} +{% if security.wpa.mode is defined %} +{% if security.wpa.mode == 'both' %} wpa=3 -{%- elif 'wpa2' in sec_wpa_mode -%} +{% elif security.wpa.mode == 'wpa2' %} wpa=2 -{%- elif 'wpa' in sec_wpa_mode -%} +{% elif security.wpa.mode == 'wpa' %} wpa=1 -{%- endif %} +{% endif %} +{% endif %} -{% if sec_wpa_cipher -%} +{% if security.wpa.cipher is defined %} # Set of accepted cipher suites (encryption algorithms) for pairwise keys # (unicast packets). This is a space separated list of algorithms: # CCMP = AES in Counter mode with CBC-MAC (CCMP-128) @@ -563,26 +497,39 @@ wpa=1 # allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, # TKIP will be used as the group cipher. The optional group_cipher parameter can # be used to override this automatic selection. -{% if 'wpa2' in sec_wpa_mode -%} + +{% if security.wpa.mode is defined and security.wpa.mode == 'wpa2' %} # Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) -rsn_pairwise={{ sec_wpa_cipher | join(" ") }} -{% else -%} +{% if security.wpa.cipher is string %} +rsn_pairwise={{ security.wpa.cipher }} +{% else %} +rsn_pairwise={{ security.wpa.cipher | join(" ") }} +{% endif %} +{% else %} # Pairwise cipher for WPA (v1) (default: TKIP) -wpa_pairwise={{ sec_wpa_cipher | join(" ") }} -{%- endif -%} -{% endif %} - -{% if sec_wpa_group_cipher -%} +{% if security.wpa.cipher is string %} +wpa_pairwise={{ security.wpa.cipher }} +{% else %} +wpa_pairwise={{ security.wpa.cipher | join(" ") }} +{% endif %} +{% endif %} +{% endif %} + +{% if security.wpa.group_cipher is defined %} # Optional override for automatic group cipher selection # This can be used to select a specific group cipher regardless of which # pairwise ciphers were enabled for WPA and RSN. It should be noted that # overriding the group cipher with an unexpected value can result in # interoperability issues and in general, this parameter is mainly used for # testing purposes. -group_cipher={{ sec_wpa_group_cipher | join(" ") }} -{% endif %} - -{% if sec_wpa_passphrase -%} +{% if security.wpa.group_cipher is string %} +group_cipher={{ security.wpa.group_cipher }} +{% else %} +group_cipher={{ security.wpa.group_cipher | join(" ") }} +{% endif %} +{% endif %} + +{% if security.wpa.passphrase is defined %} # IEEE 802.11 specifies two authentication algorithms. hostapd can be # configured to allow both of these or only one. Open system authentication # should be used with IEEE 802.1X. @@ -595,7 +542,7 @@ auth_algs=1 # secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase # (8..63 characters) that will be converted to PSK. This conversion uses SSID # so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} +wpa_passphrase={{ security.wpa.passphrase }} # Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The # entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be @@ -604,7 +551,7 @@ wpa_passphrase={{ sec_wpa_passphrase }} # WPA-PSK-SHA256 = WPA2-Personal using SHA256 wpa_key_mgmt=WPA-PSK -{% elif sec_wpa_radius -%} +{% elif security.wpa.radius is defined %} ##### IEEE 802.1X-2004 related configuration ################################## # Require IEEE 802.1X authorization ieee8021x=1 @@ -616,40 +563,37 @@ ieee8021x=1 # WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 wpa_key_mgmt=WPA-EAP -{% if sec_wpa_radius_source -%} +{% if security.wpa.radius.server is defined %} # RADIUS client forced local IP address for the access point # Normally the local IP address is determined automatically based on configured # IP addresses, but this field can be used to force a specific address to be # used, e.g., when the device has multiple IP addresses. -radius_client_addr={{ sec_wpa_radius_source }} - -# The own IP address of the access point (used as NAS-IP-Address) -own_ip_addr={{ sec_wpa_radius_source }} -{% else %} # The own IP address of the access point (used as NAS-IP-Address) +{% if security.wpa.radius.source_address is defined %} +radius_client_addr={{ security.wpa.radius.source_address }} +own_ip_addr={{ security.wpa.radius.source_address }} +{% else %} own_ip_addr=127.0.0.1 -{% endif %} +{% endif %} -{% for radius in sec_wpa_radius -%} -{%- if not radius.disabled -%} +{% for radius in security.wpa.radius.server if not radius.disabled %} # RADIUS authentication server auth_server_addr={{ radius.server }} auth_server_port={{ radius.port }} auth_server_shared_secret={{ radius.key }} -{% if radius.acc_port -%} + +{% if radius.acc_port %} # RADIUS accounting server acct_server_addr={{ radius.server }} acct_server_port={{ radius.acc_port }} acct_server_shared_secret={{ radius.key }} -{% endif %} -{% endif %} -{% endfor %} - -{% endif %} - -{% else %} +{% endif %} +{% endfor %} +{% else %} # Open system auth_algs=1 +{% endif %} +{% endif %} {% endif %} # TX queue parameters (EDCF / bursting) diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl index 2784883f1..9ddad35fd 100644 --- a/data/templates/wifi/wpa_supplicant.conf.tmpl +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -1,8 +1,8 @@ # WPA supplicant config network={ ssid="{{ ssid }}" -{%- if sec_wpa_passphrase %} - psk="{{ sec_wpa_passphrase }}" +{% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %} + psk="{{ security.wpa.passphrase }}" {% else %} key_mgmt=NONE {% endif %} diff --git a/debian/control b/debian/control index 6746fe647..3a441b47b 100644 --- a/debian/control +++ b/debian/control @@ -105,7 +105,8 @@ Depends: python3, nftables (>= 0.9.3), conntrack, libatomic1, - fastnetmon + fastnetmon, + libndp-tools Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.in index 227a043cd..e37b68514 100644 --- a/interface-definitions/include/accel-radius-additions.xml.in +++ b/interface-definitions/include/accel-radius-additions.xml.in @@ -2,6 +2,18 @@ <children> <tagNode name="server"> <children> + <leafNode name="acct-port"> + <properties> + <help>Accounting port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1813)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> <leafNode name="fail-time"> <properties> <help>Mark server unavailable for <n> seconds on failure</help> diff --git a/interface-definitions/include/bgp-afi-aggregate-address.xml.i b/interface-definitions/include/bgp-afi-aggregate-address.xml.i index 40c030fc1..050ee0074 100644 --- a/interface-definitions/include/bgp-afi-aggregate-address.xml.i +++ b/interface-definitions/include/bgp-afi-aggregate-address.xml.i @@ -1,12 +1,12 @@ -<leafNode name="as-set">
- <properties>
- <help>Generate AS-set path information for this aggregate address</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="summary-only">
- <properties>
- <help>Announce the aggregate summary network only</help>
- <valueless/>
- </properties>
-</leafNode>
+<leafNode name="as-set"> + <properties> + <help>Generate AS-set path information for this aggregate address</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="summary-only"> + <properties> + <help>Announce the aggregate summary network only</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-arp-cache-timeout.xml.i b/interface-definitions/include/interface-arp-cache-timeout.xml.i index 81d35f593..e65321158 100644 --- a/interface-definitions/include/interface-arp-cache-timeout.xml.i +++ b/interface-definitions/include/interface-arp-cache-timeout.xml.i @@ -10,4 +10,5 @@ </constraint> <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> </properties> + <defaultValue>30</defaultValue> </leafNode> diff --git a/interface-definitions/include/nat-outbound-interface.xml.i b/interface-definitions/include/nat-interface.xml.i index d562f7f03..c49483297 100644 --- a/interface-definitions/include/nat-outbound-interface.xml.i +++ b/interface-definitions/include/nat-interface.xml.i @@ -2,6 +2,7 @@ <properties> <help>Outbound interface of NAT traffic</help> <completionHelp> + <list>any</list> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> diff --git a/interface-definitions/include/source-address-ipv4-ipv6.xml.i b/interface-definitions/include/source-address-ipv4-ipv6.xml.i new file mode 100644 index 000000000..6d2d77c95 --- /dev/null +++ b/interface-definitions/include/source-address-ipv4-ipv6.xml.i @@ -0,0 +1,17 @@ +<leafNode name="source-address"> + <properties> + <help>IPv4/IPv6 source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source-address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/source-interface.xml.i b/interface-definitions/include/source-interface.xml.i new file mode 100644 index 000000000..ae579c2a6 --- /dev/null +++ b/interface-definitions/include/source-interface.xml.i @@ -0,0 +1,12 @@ +<leafNode name="source-interface"> + <properties> + <help>Physical interface used for connection</help> + <valueHelp> + <format>interface</format> + <description>Physical interface used for connection</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index ddd52979b..7d658f6a0 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -78,6 +78,7 @@ </constraint> <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage> </properties> + <defaultValue>layer2</defaultValue> </leafNode> <node name="ip"> <children> @@ -137,6 +138,7 @@ </constraint> <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage> </properties> + <defaultValue>802.3ad</defaultValue> </leafNode> <node name="member"> <properties> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 6b610e623..92356d696 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -32,6 +32,7 @@ <validator name="numeric" argument="--range 0-0 --range 10-1000000"/> </constraint> </properties> + <defaultValue>300</defaultValue> </leafNode> #include <include/interface-description.xml.i> #include <include/dhcp-options.xml.i> @@ -51,6 +52,7 @@ </constraint> <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage> </properties> + <defaultValue>14</defaultValue> </leafNode> <leafNode name="hello-time"> <properties> @@ -64,6 +66,7 @@ </constraint> <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage> </properties> + <defaultValue>2</defaultValue> </leafNode> <node name="igmp"> <properties> @@ -107,6 +110,7 @@ </constraint> <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage> </properties> + <defaultValue>20</defaultValue> </leafNode> <node name="member"> <properties> @@ -133,6 +137,7 @@ </constraint> <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage> </properties> + <defaultValue>100</defaultValue> </leafNode> <leafNode name="priority"> <properties> @@ -146,6 +151,7 @@ </constraint> <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage> </properties> + <defaultValue>32</defaultValue> </leafNode> </children> </tagNode> @@ -163,6 +169,7 @@ </constraint> <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage> </properties> + <defaultValue>32768</defaultValue> </leafNode> <leafNode name="stp"> <properties> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 1e32a15f8..e8f3f09f1 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -56,6 +56,7 @@ </constraint> <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage> </properties> + <defaultValue>auto</defaultValue> </leafNode> #include <include/interface-hw-id.xml.i> <node name="ip"> @@ -265,6 +266,7 @@ </constraint> <constraintErrorMessage>Speed must be auto, 10, 100, 1000, 2500, 5000, 10000, 25000, 40000, 50000 or 100000</constraintErrorMessage> </properties> + <defaultValue>auto</defaultValue> </leafNode> #include <include/vif-s.xml.i> #include <include/vif.xml.i> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in index 30dd9b604..3a878ad76 100644 --- a/interface-definitions/interfaces-l2tpv3.xml.in +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -29,6 +29,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>5000</defaultValue> </leafNode> #include <include/interface-disable.xml.i> <leafNode name="encapsulation"> @@ -50,6 +51,7 @@ </constraint> <constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage> </properties> + <defaultValue>udp</defaultValue> </leafNode> <node name="ipv6"> <children> @@ -138,6 +140,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>5000</defaultValue> </leafNode> <leafNode name="tunnel-id"> <properties> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index d5f9ca661..4382db598 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -70,7 +70,9 @@ </constraint> <constraintErrorMessage>mode must be private, vepa, bridge or passthru</constraintErrorMessage> </properties> + <defaultValue>private</defaultValue> </leafNode> + #include <include/interface-mtu-68-9000.xml.i> #include <include/vif-s.xml.i> #include <include/vif.xml.i> </children> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index fdde57525..8529f6885 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -64,18 +64,7 @@ </constraint> </properties> </leafNode> - <leafNode name="source-interface"> - <properties> - <help>Physical Interface used for this connection</help> - <valueHelp> - <format>interface</format> - <description>Interface used for VXLAN underlay</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/source-interface.xml.i> #include <include/interface-mtu-1200-9000.xml.i> <leafNode name="remote"> <properties> @@ -104,6 +93,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>8472</defaultValue> </leafNode> <leafNode name="vni"> <properties> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 5894f159d..981bce826 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -30,9 +30,10 @@ <description>value which marks the packet for QoS/shaper</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-255"/> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="private-key"> <properties> @@ -41,6 +42,7 @@ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> </completionHelp> </properties> + <defaultValue>default</defaultValue> </leafNode> <tagNode name="peer"> <properties> @@ -103,7 +105,11 @@ #include <include/port-number.xml.i> <leafNode name="persistent-keepalive"> <properties> - <help>how often send keep alives in seconds</help> + <help>Interval to send keepalive messages</help> + <valueHelp> + <format>1-65535</format> + <description>Interval in seconds</description> + </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 06c7734f5..6f0ec9e71 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -320,7 +320,7 @@ <properties> <help>VHT link adaptation capabilities</help> <completionHelp> - <list>unsolicited both</list> + <list>unsolicited both</list> </completionHelp> <valueHelp> <format>unsolicited</format> @@ -451,6 +451,7 @@ <leafNode name="disable-broadcast-ssid"> <properties> <help>Disable broadcast of SSID from access-point</help> + <valueless/> </properties> </leafNode> #include <include/interface-disable-link-detect.xml.i> @@ -551,9 +552,10 @@ <description>802.11ac - 1300 Mbits/sec</description> </valueHelp> <constraint> - <regex>(a|b|g|n|ac)</regex> + <regex>^(a|b|g|n|ac)$</regex> </constraint> </properties> + <defaultValue>g</defaultValue> </leafNode> <leafNode name="physical-device"> <properties> @@ -637,7 +639,7 @@ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> </valueHelp> <constraint> - <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> </constraint> <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage> <multi/> @@ -670,7 +672,7 @@ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> </valueHelp> <constraint> - <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> </constraint> <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage> <multi/> @@ -695,7 +697,7 @@ <description>Allow both WPA and WPA2</description> </valueHelp> <constraint> - <regex>(wpa|wpa2|both)</regex> + <regex>^(wpa|wpa2|both)$</regex> </constraint> <constraintErrorMessage>Unknown WPA mode</constraintErrorMessage> </properties> @@ -762,10 +764,11 @@ <description>Passively monitor all packets on the frequency/channel</description> </valueHelp> <constraint> - <regex>(access-point|station|monitor)</regex> + <regex>^(access-point|station|monitor)$</regex> </constraint> <constraintErrorMessage>Type must be access-point, station or monitor</constraintErrorMessage> </properties> + <defaultValue>monitor</defaultValue> </leafNode> #include <include/vif.xml.i> #include <include/vif-s.xml.i> diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in index 3a2899b57..8f6629d81 100644 --- a/interface-definitions/lldp.xml.in +++ b/interface-definitions/lldp.xml.in @@ -21,6 +21,7 @@ </valueHelp> <completionHelp> <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + <list>all</list> </completionHelp> </properties> <children> @@ -73,7 +74,7 @@ </completionHelp> <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage> <constraint> - <regex>(WGS84|NAD83|MLLW)$</regex> + <regex>^(WGS84|NAD83|MLLW)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 7998bd660..f8415b7c0 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -81,7 +81,7 @@ <valueless/> </properties> </leafNode> - #include <include/nat-outbound-interface.xml.i> + #include <include/nat-interface.xml.i> <node name="source"> <properties> <help>IPv6 source prefix options</help> @@ -132,7 +132,7 @@ #include <include/nat-rule.xml.i> <tagNode name="rule"> <children> - #include <include/nat-outbound-interface.xml.i> + #include <include/nat-interface.xml.i> <node name="translation"> <properties> <help>Outside NAT IP (source NAT only)</help> diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in index 62e2c87b9..8900e7955 100644 --- a/interface-definitions/protocols-bfd.xml.in +++ b/interface-definitions/protocols-bfd.xml.in @@ -28,7 +28,7 @@ <children> <node name="source"> <properties> - <help>Bind listener to specifid interface/address, mandatory for IPv6</help> + <help>Bind listener to specified interface/address, mandatory for IPv6</help> </properties> <children> <leafNode name="interface"> diff --git a/interface-definitions/mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in index a59321294..e21b1b27c 100644 --- a/interface-definitions/mdns-repeater.xml.in +++ b/interface-definitions/service_mdns-repeater.xml.in @@ -1,5 +1,4 @@ <?xml version="1.0"?> -<!-- mDNS repeater configuration --> <interfaceDefinition> <node name="service"> <children> @@ -8,7 +7,7 @@ <help>Multicast DNS (mDNS) parameters</help> </properties> <children> - <node name="repeater" owner="${vyos_conf_scripts_dir}/mdns_repeater.py"> + <node name="repeater" owner="${vyos_conf_scripts_dir}/service_mdns-repeater.py"> <properties> <help>mDNS repeater configuration</help> <priority>990</priority> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index 6a4706ab7..5a472fc9a 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -32,6 +32,7 @@ </constraint> <constraintErrorMessage>Hop count must be between 0 and 255</constraintErrorMessage> </properties> + <defaultValue>64</defaultValue> </leafNode> <leafNode name="default-lifetime"> <properties> @@ -69,10 +70,11 @@ <description>Default router has high preference</description> </valueHelp> <constraint> - <regex>(low|medium|high)</regex> + <regex>^(low|medium|high)$</regex> </constraint> <constraintErrorMessage>Default preference must be low, medium or high</constraintErrorMessage> </properties> + <defaultValue>medium</defaultValue> </leafNode> <leafNode name="dnssl"> <properties> @@ -116,6 +118,7 @@ </constraint> <constraintErrorMessage>Maximum interval must be between 4 and 1800 seconds</constraintErrorMessage> </properties> + <defaultValue>600</defaultValue> </leafNode> <leafNode name="min"> <properties> @@ -191,9 +194,10 @@ </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> - <regex>(infinity)</regex> + <regex>^(infinity)$</regex> </constraint> </properties> + <defaultValue>14400</defaultValue> </leafNode> <leafNode name="valid-lifetime"> <properties> @@ -214,6 +218,7 @@ <regex>(infinity)</regex> </constraint> </properties> + <defaultValue>2592000</defaultValue> </leafNode> </children> </tagNode> @@ -233,6 +238,7 @@ </constraint> <constraintErrorMessage>Reachable time must be 0 or between 1 and 3600000 milliseconds</constraintErrorMessage> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="retrans-timer"> <properties> @@ -250,6 +256,7 @@ </constraint> <constraintErrorMessage>Retransmit interval must be 0 or between 1 and 4294967295 milliseconds</constraintErrorMessage> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="no-send-advert"> <properties> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index 31428092f..2fe8ce583 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -11,9 +11,9 @@ <children> <tagNode name="community"> <properties> - <help>Community name [REQUIRED]</help> + <help>Community name</help> <constraint> - <regex>[a-zA-Z0-9\-_]{1,100}</regex> + <regex>^[a-zA-Z0-9\-_]{1,100}$</regex> </constraint> <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage> </properties> @@ -33,7 +33,7 @@ <description>read write</description> </valueHelp> <constraint> - <regex>(ro|rw)</regex> + <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> @@ -71,7 +71,7 @@ <properties> <help>Contact information</help> <constraint> - <regex>.{1,255}</regex> + <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Contact information is limited to 255 characters or less</constraintErrorMessage> </properties> @@ -80,7 +80,7 @@ <properties> <help>Description information</help> <constraint> - <regex>.{1,255}</regex> + <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Description is limited to 255 characters or less</constraintErrorMessage> </properties> @@ -121,7 +121,7 @@ <properties> <help>Location information</help> <constraint> - <regex>.{1,255}</regex> + <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage> </properties> @@ -197,9 +197,9 @@ <children> <leafNode name="engineid"> <properties> - <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help> + <help>Specifies the EngineID that uniquely identify an agent (e.g. 000000000000000000000002)</help> <constraint> - <regex>(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex> + <regex>^([0-9a-f][0-9a-f]){1,18}$</regex> </constraint> <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> </properties> @@ -224,7 +224,7 @@ <description>read write</description> </valueHelp> <constraint> - <regex>(ro|rw)</regex> + <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> @@ -233,7 +233,7 @@ <properties> <help>Security levels</help> <completionHelp> - <list>noauth auth priv2</list> + <list>noauth auth priv</list> </completionHelp> <valueHelp> <format>noauth</format> @@ -248,7 +248,7 @@ <description>Messages are authenticated and encrypted (authPriv)</description> </valueHelp> <constraint> - <regex>(noauth|auth|priv)</regex> + <regex>^(noauth|auth|priv)$</regex> </constraint> </properties> </leafNode> @@ -284,20 +284,20 @@ <help>Defines the privacy</help> </properties> <children> - <leafNode name="encrypted-key"> + <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for authentication</help> <constraint> - <regex>0x[0-9a-f]*$</regex> + <regex>^[0-9a-f]*$</regex> </constraint> - <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> - <leafNode name="plaintext-key"> + <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for authentication</help> <constraint> - <regex>.{8,}$</regex> + <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> @@ -317,7 +317,7 @@ <description>Secure Hash Algorithm</description> </valueHelp> <constraint> - <regex>(md5|sha)</regex> + <regex>^(md5|sha)$</regex> </constraint> </properties> </leafNode> @@ -341,20 +341,20 @@ <help>Defines the privacy</help> </properties> <children> - <leafNode name="encrypted-key"> + <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for privacy protocol</help> <constraint> - <regex>0x[0-9a-f]*$</regex> + <regex>^[0-9a-f]*$</regex> </constraint> - <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> - <leafNode name="plaintext-key"> + <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for privacy protocol</help> <constraint> - <regex>.{8,}$</regex> + <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> @@ -374,7 +374,7 @@ <description>Advanced Encryption Standard</description> </valueHelp> <constraint> - <regex>(des|aes)</regex> + <regex>^(des|aes)$</regex> </constraint> </properties> </leafNode> @@ -395,7 +395,7 @@ <description>Use User Datagram Protocol for notifications</description> </valueHelp> <constraint> - <regex>(tcp|udp)</regex> + <regex>^(tcp|udp)$</regex> </constraint> </properties> </leafNode> @@ -414,7 +414,7 @@ <description>Use TRAP</description> </valueHelp> <constraint> - <regex>(inform|trap)</regex> + <regex>^(inform|trap)$</regex> </constraint> </properties> </leafNode> @@ -442,20 +442,20 @@ <help>Specifies the auth</help> </properties> <children> - <leafNode name="encrypted-key"> + <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for authentication</help> <constraint> - <regex>0x[0-9a-f]*$</regex> + <regex>^[0-9a-f]*$</regex> </constraint> - <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> - <leafNode name="plaintext-key"> + <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for authentication</help> <constraint> - <regex>.{8,}$</regex> + <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> @@ -475,7 +475,7 @@ <description>Secure Hash Algorithm</description> </valueHelp> <constraint> - <regex>(md5|sha)</regex> + <regex>^(md5|sha)$</regex> </constraint> </properties> </leafNode> @@ -504,7 +504,7 @@ <description>read write</description> </valueHelp> <constraint> - <regex>(ro|rw)</regex> + <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> @@ -514,20 +514,20 @@ <help>Defines the privacy</help> </properties> <children> - <leafNode name="encrypted-key"> + <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for privacy protocol</help> <constraint> - <regex>0x[0-9a-f]*$</regex> + <regex>^[0-9a-f]*$</regex> </constraint> - <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> - <leafNode name="plaintext-key"> + <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for privacy protocol</help> <constraint> - <regex>.{8,}$</regex> + <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> @@ -547,7 +547,7 @@ <description>Advanced Encryption Standard</description> </valueHelp> <constraint> - <regex>(des|aes)</regex> + <regex>^(des|aes)$</regex> </constraint> </properties> </leafNode> @@ -568,7 +568,7 @@ <properties> <help>Specifies the oid</help> <constraint> - <regex>[0-9]+(\.[0-9]+)*$</regex> + <regex>^[0-9]+(\.[0-9]+)*$</regex> </constraint> <constraintErrorMessage>OID must start from a number</constraintErrorMessage> </properties> @@ -582,7 +582,7 @@ <properties> <help>Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant</help> <constraint> - <regex>[0-9a-f]{2}([\.:][0-9a-f]{2})*$</regex> + <regex>^[0-9a-f]{2}([\.:][0-9a-f]{2})*$</regex> </constraint> <constraintErrorMessage>MASK is a list of hex octets, separated by '.' or ':'</constraintErrorMessage> </properties> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index 1b20f5776..d253c2f34 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -132,30 +132,30 @@ <properties> <help>Log level</help> <completionHelp> - <list>QUIET FATAL ERROR INFO VERBOSE</list> + <list>quiet fatal error info verbose</list> </completionHelp> <valueHelp> - <format>QUIET</format> + <format>quiet</format> <description>stay silent</description> </valueHelp> <valueHelp> - <format>FATAL</format> + <format>fatal</format> <description>log fatals only</description> </valueHelp> <valueHelp> - <format>ERROR</format> + <format>error</format> <description>log errors and fatals only</description> </valueHelp> <valueHelp> - <format>INFO</format> + <format>info</format> <description>default log level</description> </valueHelp> <valueHelp> - <format>VERBOSE</format> + <format>verbose</format> <description>enable logging of failed login attempts</description> </valueHelp> <constraint> - <regex>^(QUIET|FATAL|ERROR|INFO|VERBOSE)$</regex> + <regex>^(quiet|fatal|error|info|verbose)$</regex> </constraint> </properties> <defaultValue>INFO</defaultValue> diff --git a/interface-definitions/system-options.xml.in b/interface-definitions/system-options.xml.in index 48bc353ab..a5fec10db 100644 --- a/interface-definitions/system-options.xml.in +++ b/interface-definitions/system-options.xml.in @@ -33,7 +33,7 @@ <description>Poweroff VyOS</description> </valueHelp> <constraint> - <regex>(ignore|reboot|poweroff)</regex> + <regex>^(ignore|reboot|poweroff)$</regex> </constraint> <constraintErrorMessage>Must be ignore, reboot, or poweroff</constraintErrorMessage> </properties> @@ -44,6 +44,23 @@ <valueless/> </properties> </leafNode> + <node name="http-client"> + <properties> + <help>Global options used for HTTP client</help> + </properties> + <children> + #include <include/source-interface.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + </children> + </node> + <node name="ssh-client"> + <properties> + <help>Global options used for SSH client</help> + </properties> + <children> + #include <include/source-address-ipv4-ipv6.xml.i> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/add-system-image.xml b/op-mode-definitions/add-system-image.xml new file mode 100644 index 000000000..3dc1c67ab --- /dev/null +++ b/op-mode-definitions/add-system-image.xml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="add"> + <children> + <node name="system"> + <properties> + <help>Add item to a system facility</help> + </properties> + <children> + <tagNode name="image"> + <properties> + <help>Add a new image to the system</help> + <completionHelp> + <list>/path/to/vyos-image.iso http://example.com/vyos-image.iso</list> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}"</command> + <children> + <tagNode name="vrf"> + <properties> + <help>Download image via specified VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}"</command> + <children> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --username "${6}" --password "${8}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/configure.xml b/op-mode-definitions/configure.xml new file mode 100644 index 000000000..3dd5a0f45 --- /dev/null +++ b/op-mode-definitions/configure.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="configure"> + <properties> + <help>Enter configuration mode</help> + </properties> + <command>if [ `id -u` == 0 ]; then + echo "You are attempting to enter configuration mode as root." + echo "It may have unintended consequences and render your system" + echo "unusable until restart." + echo "Please do it as an administrator level VyOS user instead." + else + if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then + echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed" + fi + history -w + export _OFR_CONFIGURE=ok + newgrp vyattacfg + unset _OFR_CONFIGURE + _vyatta_op_do_key_bindings + history -r + fi</command> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/connect-disconnect.xml b/op-mode-definitions/connect.xml index f0fcef5da..1ec62949a 100644 --- a/op-mode-definitions/connect-disconnect.xml +++ b/op-mode-definitions/connect.xml @@ -2,35 +2,27 @@ <interfaceDefinition> <node name="connect"> <properties> - <help>Establish a connection</help> + <help>Establish connection</help> </properties> <children> - <tagNode name="interface"> + <tagNode name="console"> <properties> - <help>Bring up a connection-oriented network interface</help> + <help>Connect to device attached to serial console server</help> <completionHelp> - <path>interfaces pppoe</path> - <path>interfaces wirelessmodem</path> + <path>service console-server device</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> + <command>/usr/bin/console "$3"</command> </tagNode> - </children> - </node> - <node name="disconnect"> - <properties> - <help>Take down a connection</help> - </properties> - <children> <tagNode name="interface"> <properties> - <help>Take down a connection-oriented network interface</help> + <help>Bring up a connection-oriented network interface</help> <completionHelp> <path>interfaces pppoe</path> <path>interfaces wirelessmodem</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/disconnect.xml b/op-mode-definitions/disconnect.xml new file mode 100644 index 000000000..bf2c37b89 --- /dev/null +++ b/op-mode-definitions/disconnect.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="disconnect"> + <properties> + <help>Take down a connection</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Take down a connection-oriented network interface</help> + <completionHelp> + <path>interfaces pppoe</path> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml index 3eadabf0a..c7bcad413 100644 --- a/op-mode-definitions/force-arp.xml +++ b/op-mode-definitions/force-arp.xml @@ -27,13 +27,13 @@ <properties> <help>Send gratuitous ARP reply for specified address</help> </properties> - <command>sudo arping -I $5 -c 1 -A $7</command> + <command>sudo /usr/bin/arping -I $5 -c 1 -A $7</command> <children> <tagNode name="count"> <properties> <help>Send specified number of ARP replies</help> </properties> - <command>sudo arping -I $5 -c $9 -A $7</command> + <command>sudo /usr/bin/arping -I $5 -c $9 -A $7</command> </tagNode> </children> </tagNode> @@ -58,13 +58,13 @@ <properties> <help>Send gratuitous ARP request for specified address</help> </properties> - <command>sudo arping -I $5 -c 1 -U $7</command> + <command>sudo /usr/bin/arping -I $5 -c 1 -U $7</command> <children> <tagNode name="count"> <properties> <help>Send specified number of ARP requests</help> </properties> - <command>sudo arping -I $5 -c $9 -U $7</command> + <command>sudo /usr/bin/arping -I $5 -c $9 -U $7</command> </tagNode> </children> </tagNode> diff --git a/op-mode-definitions/force-ipv6-nd.xml b/op-mode-definitions/force-ipv6-nd.xml new file mode 100644 index 000000000..49de097f6 --- /dev/null +++ b/op-mode-definitions/force-ipv6-nd.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-nd"> + <properties> + <help>IPv6 Neighbor Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Neighbor Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of node to lookup</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/ndisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-ipv6-rd.xml b/op-mode-definitions/force-ipv6-rd.xml new file mode 100644 index 000000000..8c901af25 --- /dev/null +++ b/op-mode-definitions/force-ipv6-rd.xml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-rd"> + <properties> + <help>IPv6 Router Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Router Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 "$4"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of target</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/lldp.xml b/op-mode-definitions/lldp.xml index 105bfe237..297ccf1f4 100644 --- a/op-mode-definitions/lldp.xml +++ b/op-mode-definitions/lldp.xml @@ -17,7 +17,7 @@ <properties> <help>Show LLDP neighbor details</help> </properties> - <command>/usr/sbin/lldpctl -f plain</command> + <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command> </node> <tagNode name="interface"> <properties> @@ -26,7 +26,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/lldp_op.py --interface $4</command> + <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml index d1e459b17..d1e459b17 100644 --- a/op-mode-definitions/bandwidth-test.xml +++ b/op-mode-definitions/monitor-bandwidth-test.xml diff --git a/op-mode-definitions/bandwidth-monitor.xml b/op-mode-definitions/monitor-bandwidth.xml index 9af0a9e70..9af0a9e70 100644 --- a/op-mode-definitions/bandwidth-monitor.xml +++ b/op-mode-definitions/monitor-bandwidth.xml diff --git a/op-mode-definitions/monitor-log.xml b/op-mode-definitions/monitor-log.xml new file mode 100644 index 000000000..99efe5306 --- /dev/null +++ b/op-mode-definitions/monitor-log.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <properties> + <help>Monitor last lines of messages file</help> + </properties> + <command>tail --follow=name /var/log/messages</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-ndp.xml b/op-mode-definitions/monitor-ndp.xml new file mode 100644 index 000000000..e25eccf3a --- /dev/null +++ b/op-mode-definitions/monitor-ndp.xml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="ndp"> + <properties> + <help>Monitors the NDP information received by the router through the device</help> + </properties> + <command>sudo ndptool monitor</command> + <children> + <tagNode name="interface"> + <command>sudo ndptool monitor --ifname=$4</command> + <properties> + <help>Monitor ndp protocol on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="type"> + <command>sudo ndptool monitor --ifname=$4 --msg-type=$6</command> + <properties> + <help>Monitor ndp protocol on specified interface</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </tagNode> + <tagNode name="type"> + <command>sudo ndptool monitor --msg-type=$4</command> + <properties> + <help>Monitor ndp protocol on specified interface</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ping.xml b/op-mode-definitions/ping.xml new file mode 100644 index 000000000..4c25a59ab --- /dev/null +++ b/op-mode-definitions/ping.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <tagNode name="ping"> + <properties> + <help>Send Internet Control Message Protocol (ICMP) echo request</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>Ping options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/ping.py --get-options "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + </leafNode> + </children> + </tagNode> +</interfaceDefinition> diff --git a/op-mode-definitions/show-console-server.xml b/op-mode-definitions/show-console-server.xml index e47b6cfaa..77a7f3376 100644 --- a/op-mode-definitions/show-console-server.xml +++ b/op-mode-definitions/show-console-server.xml @@ -1,18 +1,5 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="connect"> - <children> - <tagNode name="console"> - <properties> - <help>Connect to device attached to serial console server</help> - <completionHelp> - <path>service console-server device</path> - </completionHelp> - </properties> - <command>/usr/bin/console "$3"</command> - </tagNode> - </children> - </node> <node name="show"> <children> <node name="log"> diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml index a49036397..c3ff3a60f 100644 --- a/op-mode-definitions/show-hardware.xml +++ b/op-mode-definitions/show-hardware.xml @@ -71,14 +71,20 @@ <properties> <help>Show peripherals connected to the USB bus</help> </properties> - <command>lsusb</command> + <command>/usr/bin/lsusb -t</command> <children> <node name="detail"> <properties> <help>Show detailed USB bus information</help> </properties> - <command>lsusb -v</command> + <command>/usr/bin/lsusb -v</command> </node> + <leafNode name="serial"> + <properties> + <help>Show information about connected USB serial ports</help> + </properties> + <command>${vyos_op_scripts_dir}/show_usb_serial.py</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-ethernet.xml b/op-mode-definitions/show-interfaces-ethernet.xml index 80f07c2bd..bdcfa55f1 100644 --- a/op-mode-definitions/show-interfaces-ethernet.xml +++ b/op-mode-definitions/show-interfaces-ethernet.xml @@ -74,6 +74,7 @@ <properties> <help>Show ethernet interface information</help> </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show-brief</command> <children> <leafNode name="detail"> <properties> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml index 01acd4fc6..393ca912f 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -26,10 +26,24 @@ <path>interfaces pppoe</path> </completionHelp> </properties> - <command>/usr/sbin/pppstats $4</command> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> </leafNode> </children> </tagNode> + <node name="pppoe"> + <properties> + <help>Show PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml index 1f710b3dc..c0ab9c66f 100644 --- a/op-mode-definitions/show-interfaces-wirelessmodem.xml +++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml @@ -26,10 +26,24 @@ <path>interfaces wirelessmodem</path> </completionHelp> </properties> - <command>/usr/sbin/pppstats $4</command> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> </leafNode> </children> </tagNode> + <node name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Wireless Modem (WWAN( interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-ipv6-bgp.xml b/op-mode-definitions/show-ipv6-bgp.xml new file mode 100644 index 000000000..67a8c8658 --- /dev/null +++ b/op-mode-definitions/show-ipv6-bgp.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ipv6"> + <children> + <node name="bgp"> + <children> + <tagNode name="route-map"> + <properties> + <help>Show BGP routes matching the specified route map</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp ipv6 route-map $5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-system.xml b/op-mode-definitions/show-system.xml index 74b34ae92..1b98b559b 100644 --- a/op-mode-definitions/show-system.xml +++ b/op-mode-definitions/show-system.xml @@ -176,20 +176,6 @@ </properties> <command>uptime</command> </leafNode> - <node name="usb"> - <properties> - <help>Show information about Universal Serial Bus (USB)</help> - </properties> - <command>/usr/bin/lsusb -t</command> - <children> - <leafNode name="serial"> - <properties> - <help>Show information about connected USB serial ports</help> - </properties> - <command>${vyos_op_scripts_dir}/show_usb_serial.py</command> - </leafNode> - </children> - </node> </children> </node> </children> diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml index 1c806908b..438e7c334 100644 --- a/op-mode-definitions/show-vrf.xml +++ b/op-mode-definitions/show-vrf.xml @@ -11,9 +11,9 @@ <tagNode name="vrf"> <properties> <help>Show information on specific VRF instance</help> - <completionHelp> - <path>vrf name</path> - </completionHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> </properties> <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> <children> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml index 340d28280..efbaaeb2f 100644 --- a/op-mode-definitions/traceroute.xml +++ b/op-mode-definitions/traceroute.xml @@ -25,6 +25,24 @@ </completionHelp> </properties> <command>/usr/bin/traceroute -4 "$3"</command> + <children> + <tagNode name="tcp"> + <properties> + <help>Route tracing and port detection using TCP</help> + </properties> + <children> + <tagNode name="port"> + <properties> + <help>Perform operations on TCP ports</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/tcptraceroute "$3" $6</command> + </tagNode> + </children> + </tagNode> + </children> </tagNode> <tagNode name="ipv6"> <properties> @@ -34,6 +52,24 @@ </completionHelp> </properties> <command>/usr/bin/traceroute -6 "$3"</command> + <children> + <tagNode name="tcp"> + <properties> + <help>Use TCP/IPv6 packets to perform a traceroute</help> + </properties> + <children> + <tagNode name="port"> + <properties> + <help>Perform operations on TCP ports</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/tcptraceroute6 "$3" $6</command> + </tagNode> + </children> + </tagNode> + </children> </tagNode> <tagNode name="vrf"> <properties> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 1795fb820..a7bfa36a3 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -96,6 +96,20 @@ <!-- more commands upon request --> </children> </tagNode> + <node name="wireguard"> + <properties> + <help>Show wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/python/vyos/config.py b/python/vyos/config.py index 780b48a7b..884d6d947 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -63,22 +63,13 @@ In operational mode, all functions return values from the running config. """ -import os import re import json -import subprocess +from copy import deepcopy import vyos.util import vyos.configtree - -class VyOSError(Exception): - """ - Raised on config access errors, most commonly if the type of a config tree node - in the system does not match the type of operation. - - """ - pass - +from vyos.configsource import ConfigSource, ConfigSourceSession class Config(object): """ @@ -88,49 +79,18 @@ class Config(object): the only state it keeps is relative *config path* for convenient access to config subtrees. """ - def __init__(self, session_env=None): - self._cli_shell_api = "/bin/cli-shell-api" - self._level = [] - if session_env: - self.__session_env = session_env - else: - self.__session_env = None - - # Running config can be obtained either from op or conf mode, it always succeeds - # once the config system is initialized during boot; - # before initialization, set to empty string - if os.path.isfile('/tmp/vyos-config-status'): - try: - running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - running_config_text = '' - else: - running_config_text = '' - - # Session config ("active") only exists in conf mode. - # In op mode, we'll just use the same running config for both active and session configs. - if self.in_session(): - try: - session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - session_config_text = '' - else: - session_config_text = running_config_text - - if running_config_text: - self._running_config = vyos.configtree.ConfigTree(running_config_text) - else: - self._running_config = None - - if session_config_text: - self._session_config = vyos.configtree.ConfigTree(session_config_text) + def __init__(self, session_env=None, config_source=None): + if config_source is None: + self._config_source = ConfigSourceSession(session_env) else: - self._session_config = None + if not isinstance(config_source, ConfigSource): + raise TypeError("config_source not of type ConfigSource") + self._config_source = config_source - def _make_command(self, op, path): - args = path.split() - cmd = [self._cli_shell_api, op] + args - return cmd + self._level = [] + self._dict_cache = {} + (self._running_config, + self._session_config) = self._config_source.get_configtree_tuple() def _make_path(self, path): # Backwards-compatibility stuff: original implementation used string paths @@ -146,19 +106,6 @@ class Config(object): raise TypeError("Path must be a whitespace-separated string or a list") return (self._level + path) - def _run(self, cmd): - if self.__session_env: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) - else: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - out = p.stdout.read() - p.wait() - p.communicate() - if p.returncode != 0: - raise VyOSError() - else: - return out.decode('ascii') - def set_level(self, path): """ Set the *edit level*, that is, a relative config tree path. @@ -226,22 +173,14 @@ class Config(object): Returns: True if the config session has uncommited changes, False otherwise. """ - try: - self._run(self._make_command('sessionChanged', '')) - return True - except VyOSError: - return False + return self._config_source.session_changed() def in_session(self): """ Returns: True if called from a configuration session, False otherwise. """ - try: - self._run(self._make_command('inSession', '')) - return True - except VyOSError: - return False + return self._config_source.in_session() def show_config(self, path=[], default=None, effective=False): """ @@ -252,40 +191,25 @@ class Config(object): Returns: str: working configuration """ + return self._config_source.show_config(path, default, effective) - # show_config should be independent of CLI edit level. - # Set the CLI edit environment to the top level, and - # restore original on exit. - save_env = self.__session_env + def get_cached_dict(self, effective=False): + cached = self._dict_cache.get(effective, {}) + if cached: + config_dict = cached + else: + config_dict = {} - env_str = self._run(self._make_command('getEditResetEnv', '')) - env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) - root_env = os.environ - for k, v in env_list: - root_env[k] = v + if effective: + if self._running_config: + config_dict = json.loads((self._running_config).to_json()) + else: + if self._session_config: + config_dict = json.loads((self._session_config).to_json()) - self.__session_env = root_env + self._dict_cache[effective] = config_dict - # FIXUP: by default, showConfig will give you a diff - # if there are uncommitted changes. - # The config parser obviously cannot work with diffs, - # so we need to supress diff production using appropriate - # options for getting either running (active) - # or proposed (working) config. - if effective: - path = ['--show-active-only'] + path - else: - path = ['--show-working-only'] + path - - if isinstance(path, list): - path = " ".join(path) - try: - out = self._run(self._make_command('showConfig', path)) - self.__session_env = save_env - return out - except VyOSError: - self.__session_env = save_env - return(default) + return config_dict def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False): """ @@ -297,14 +221,7 @@ class Config(object): Returns: a dict representation of the config under path """ - config_dict = {} - - if effective: - if self._running_config: - config_dict = json.loads((self._running_config).to_json()) - else: - if self._session_config: - config_dict = json.loads((self._session_config).to_json()) + config_dict = self.get_cached_dict(effective) config_dict = vyos.util.get_sub_dict(config_dict, self._make_path(path), get_first_key) @@ -316,6 +233,8 @@ class Config(object): raise ValueError("key_mangling must be a tuple of two strings") else: config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1]) + else: + config_dict = deepcopy(config_dict) return config_dict @@ -330,12 +249,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isMulti', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_multi(path) def is_tag(self, path): """ @@ -348,12 +263,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isTag', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_tag(path) def is_leaf(self, path): """ @@ -366,12 +277,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isLeaf', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_leaf(path) def return_value(self, path, default=None): """ @@ -532,9 +439,6 @@ class Config(object): Returns: str list: child node names - - Raises: - VyOSError: if the node is not a tag node """ if self._running_config: try: diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 0dc7578d8..126d6195a 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -15,15 +15,15 @@ """ A library for retrieving value dicts from VyOS configs in a declarative fashion. - """ +import os +import jmespath from enum import Enum from copy import deepcopy from vyos import ConfigError from vyos.validate import is_member -from vyos.util import ifname_from_config def retrieve_config(path_hash, base_path, config): """ @@ -102,397 +102,173 @@ def dict_merge(source, destination): return tmp def list_diff(first, second): - """ - Diff two dictionaries and return only unique items - """ + """ Diff two dictionaries and return only unique items """ second = set(second) return [item for item in first if item not in second] - -def get_ethertype(ethertype_val): - if ethertype_val == '0x88A8': - return '802.1ad' - elif ethertype_val == '0x8100': - return '802.1q' - else: - raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) - -dhcpv6_pd_default_data = { - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'dhcpv6_pd_length': '', - 'dhcpv6_pd_interfaces': [] -} - -interface_default_data = { - **dhcpv6_pd_default_data, - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'disable': False, - 'disable_link_detect': 1, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_eui64_prefix_remove': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'mac': '', - 'mtu': 1500, - 'vrf': '' -} - -vlan_default = { - **interface_default_data, - 'egress_qos': '', - 'egress_qos_changed': False, - 'ingress_qos': '', - 'ingress_qos_changed': False, - 'vif_c': {}, - 'vif_c_remove': [] -} - -# see: https://docs.python.org/3/library/enum.html#functional-api -disable = Enum('disable','none was now both') - -def disable_state(conf, check=[3,5,7]): +def T2665_default_dict_cleanup(dict): + """ Cleanup default keys for tag nodes https://phabricator.vyos.net/T2665. """ + # Cleanup + for vif in ['vif', 'vif_s']: + if vif in dict.keys(): + for key in ['ip', 'mtu']: + if key in dict[vif].keys(): + del dict[vif][key] + + # cleanup VIF-S defaults + if 'vif_c' in dict[vif].keys(): + for key in ['ip', 'mtu']: + if key in dict[vif]['vif_c'].keys(): + del dict[vif]['vif_c'][key] + # If there is no vif-c defined and we just cleaned the default + # keys - we can clean the entire vif-c dict as it's useless + if not dict[vif]['vif_c']: + del dict[vif]['vif_c'] + + # If there is no real vif/vif-s defined and we just cleaned the default + # keys - we can clean the entire vif dict as it's useless + if not dict[vif]: + del dict[vif] + + return dict + +def leaf_node_changed(conf, path): """ - return if and how a particual section of the configuration is has disable'd - using "disable" including if it was disabled by one of its parent. - - check: a list of the level we should check, here 7,5 and 3 - interfaces ethernet eth1 vif-s 1 vif-c 2 disable - interfaces ethernet eth1 vif 1 disable - interfaces ethernet eth1 disable - - it returns an enum (none, was, now, both) + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned """ - - # save where we are in the config - current_level = conf.get_level() - - # logic to figure out if the interface (or one of it parent is disabled) - eff_disable = False - act_disable = False - - levels = check[:] - working_level = current_level[:] - - while levels: - position = len(working_level) - if not position: - break - if position not in levels: - working_level = working_level[:-1] - continue - - levels.remove(position) - conf.set_level(working_level) - working_level = working_level[:-1] - - eff_disable = eff_disable or conf.exists_effective('disable') - act_disable = act_disable or conf.exists('disable') - - conf.set_level(current_level) - - # how the disabling changed - if eff_disable and act_disable: - return disable.both - if eff_disable and not eff_disable: - return disable.was - if not eff_disable and act_disable: - return disable.now - return disable.none - - -def intf_to_dict(conf, default): - from vyos.ifconfig import Interface - + from vyos.configdiff import get_config_diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + (new, old) = D.get_value_diff(path) + if new != old: + if isinstance(old, str): + return old + elif isinstance(old, list): + if isinstance(new, str): + new = [new] + elif isinstance(new, type(None)): + new = [] + return list_diff(old, new) + + return None + +def node_changed(conf, path): """ - Common used function which will extract VLAN related information from config - and represent the result as Python dictionary. - - Function call's itself recursively if a vif-s/vif-c pair is detected. + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned """ + from vyos.configdiff import get_config_diff, Diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys() + return list(keys) + +def get_removed_vlans(conf, dict): + """ + Common function to parse a dictionary retrieved via get_config_dict() and + determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q. + """ + from vyos.configdiff import get_config_diff, Diff - intf = deepcopy(default) - intf['intf'] = ifname_from_config(conf) - - current_vif_list = conf.list_nodes(['vif']) - previous_vif_list = conf.list_effective_nodes(['vif']) - - # set the vif to be deleted - for vif in previous_vif_list: - if vif not in current_vif_list: - intf['vif_remove'].append(vif) - - # retrieve interface description - if conf.exists(['description']): - intf['description'] = conf.return_value(['description']) - - # get DHCP client identifier - if conf.exists(['dhcp-options', 'client-id']): - intf['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id']) - - # DHCP client host name (overrides the system host name) - if conf.exists(['dhcp-options', 'host-name']): - intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name']) - - # DHCP client vendor identifier - if conf.exists(['dhcp-options', 'vendor-class-id']): - intf['dhcp_vendor_class_id'] = conf.return_value( - ['dhcp-options', 'vendor-class-id']) - - # DHCPv6 only acquire config parameters, no address - if conf.exists(['dhcpv6-options', 'parameters-only']): - intf['dhcpv6_prm_only'] = True - - # DHCPv6 prefix delegation (RFC3633) - current_level = conf.get_level() - if conf.exists(['dhcpv6-options', 'prefix-delegation']): - dhcpv6_pd_path = current_level + ['dhcpv6-options', 'prefix-delegation'] - conf.set_level(dhcpv6_pd_path) - - # retriebe DHCPv6-PD prefix helper length as some ISPs only hand out a - # /64 by default (https://phabricator.vyos.net/T2506) - if conf.exists(['length']): - intf['dhcpv6_pd_length'] = conf.return_value(['length']) + # Check vif, vif-s/vif-c VLAN interfaces for removal + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_remove': [*keys]}) - for interface in conf.list_nodes(['interface']): - conf.set_level(dhcpv6_pd_path + ['interface', interface]) - pd = { - 'ifname': interface, - 'sla_id': '', - 'sla_len': '', - 'if_id': '' - } + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s_remove': [*keys]}) - if conf.exists(['sla-id']): - pd['sla_id'] = conf.return_value(['sla-id']) + for vif in dict.get('vif_s', {}).keys(): + keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}}) - if conf.exists(['sla-len']): - pd['sla_len'] = conf.return_value(['sla-len']) + return dict - if conf.exists(['address']): - pd['if_id'] = conf.return_value(['address']) - - intf['dhcpv6_pd_interfaces'].append(pd) - - # re-set config level - conf.set_level(current_level) - - # DHCPv6 temporary IPv6 address - if conf.exists(['dhcpv6-options', 'temporary']): - intf['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists(['disable-link-detect']): - intf['disable_link_detect'] = 2 - - # ARP filter configuration - if conf.exists(['ip', 'disable-arp-filter']): - intf['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists(['ip', 'enable-arp-accept']): - intf['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists(['ip', 'enable-arp-announce']): - intf['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists(['ip', 'enable-arp-ignore']): - intf['ip_enable_arp_ignore'] = 1 - - # Enable Proxy ARP - if conf.exists(['ip', 'enable-proxy-arp']): - intf['ip_proxy_arp'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists(['ipv6', 'address', 'autoconf']): - intf['ipv6_autoconf'] = 1 - - # Disable IPv6 forwarding on this interface - if conf.exists(['ipv6', 'disable-forwarding']): - intf['ipv6_forwarding'] = 0 - - # check if interface is member of a bridge - intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge') - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists(['ipv6', 'dup-addr-detect-transmits']): - intf['ipv6_dup_addr_detect'] = int( - conf.return_value(['ipv6', 'dup-addr-detect-transmits'])) - - # Media Access Control (MAC) address - if conf.exists(['mac']): - intf['mac'] = conf.return_value(['mac']) - - # Maximum Transmission Unit (MTU) - if conf.exists(['mtu']): - intf['mtu'] = int(conf.return_value(['mtu'])) - - # retrieve VRF instance - if conf.exists(['vrf']): - intf['vrf'] = conf.return_value(['vrf']) - - # egress QoS - if conf.exists(['egress-qos']): - intf['egress_qos'] = conf.return_value(['egress-qos']) - - # egress changes QoS require VLAN interface recreation - if conf.return_effective_value(['egress-qos']): - if intf['egress_qos'] != conf.return_effective_value(['egress-qos']): - intf['egress_qos_changed'] = True - - # ingress QoS - if conf.exists(['ingress-qos']): - intf['ingress_qos'] = conf.return_value(['ingress-qos']) - - # ingress changes QoS require VLAN interface recreation - if conf.return_effective_value(['ingress-qos']): - if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']): - intf['ingress_qos_changed'] = True - - # Get the interface addresses - intf['address'] = conf.return_values(['address']) - - # addresses to remove - difference between effective and working config - intf['address_remove'] = list_diff( - conf.return_effective_values(['address']), intf['address']) - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64']) - - # EUI64 to remove - difference between effective and working config - intf['ipv6_eui64_prefix_remove'] = list_diff( - conf.return_effective_values(['ipv6', 'address', 'eui64']), - intf['ipv6_eui64_prefix']) - - # Determine if the interface should be disabled - disabled = disable_state(conf) - if disabled == disable.both: - # was and is still disabled - intf['disable'] = True - elif disabled == disable.now: - # it is now disable but was not before - intf['disable'] = True - elif disabled == disable.was: - # it was disable but not anymore - intf['disable'] = False - else: - # normal change - intf['disable'] = False - - # Remove the default link-local address if no-default-link-local is set, - # if member of a bridge or if disabled (it may not have a MAC if it's down) - if ( conf.exists(['ipv6', 'address', 'no-default-link-local']) - or intf.get('is_bridge_member') or intf['disable'] ): - intf['ipv6_eui64_prefix_remove'].append('fe80::/64') - else: - # add the link-local by default to make IPv6 work - intf['ipv6_eui64_prefix'].append('fe80::/64') - - # If MAC has changed, remove and re-add all IPv6 EUI64 addresses - try: - interface = Interface(intf['intf'], create=False) - if intf['mac'] and intf['mac'] != interface.get_mac(): - intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix'] - except Exception: - # If the interface does not exist, it could not have changed - pass - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']: - intf['ipv6_accept_ra'] = 2 - - return intf, disable - - - -def add_to_dict(conf, disabled, ifdict, section, key): +def get_interface_dict(config, base, ifname=''): """ - parse a section of vif/vif-s/vif-c and add them to the dict - follow the convention to: - * use the "key" for what to add - * use the "key" what what to remove - - conf: is the Config() already at the level we need to parse - disabled: is a disable enum so we know how to handle to data - intf: if the interface dictionary - section: is the section name to parse (vif/vif-s/vif-c) - key: is the dict key to use (vif/vifs/vifc) + Common utility function to retrieve and mandgle the interfaces available + in CLI configuration. All interfaces have a common base ground where the + value retrival is identical - so it can and should be reused + + Will return a dictionary with the necessary interface configuration """ + from vyos.xml import defaults + + if not ifname: + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # retrieve interface default values + default_values = defaults(base) + + # setup config level which is extracted in get_removed_vlans() + config.set_level(base + [ifname]) + dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + + # Check if interface has been removed + if dict == {}: + dict.update({'deleted' : ''}) + + # Add interface instance name into dictionary + dict.update({'ifname': ifname}) + + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary + # retrived. + dict = dict_merge(default_values, dict) + + # Check if we are a member of a bridge device + bridge = is_member(config, ifname, 'bridge') + if bridge: + dict.update({'is_bridge_member' : bridge}) + + # Check if we are a member of a bond device + bond = is_member(config, ifname, 'bonding') + if bond: + dict.update({'is_bond_member' : bond}) + + mac = leaf_node_changed(config, ['mac']) + if mac: + dict.update({'mac_old' : mac}) + + eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) + if eui64: + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(eui64, str): + eui64 = [eui64] + tmp = jmespath.search('ipv6.address', dict) + if not tmp: + dict.update({'ipv6': {'address': {'eui64_old': eui64}}}) + else: + dict['ipv6']['address'].update({'eui64_old': eui64}) + + # remove wrongly inserted values + dict = T2665_default_dict_cleanup(dict) + + # The values are identical for vif, vif-s and vif-c as the all include the same + # XML definitions which hold the defaults + default_vif_values = defaults(base + ['vif']) + for vif, vif_config in dict.get('vif', {}).items(): + vif_config = dict_merge(default_vif_values, vif_config) + for vif_s, vif_s_config in dict.get('vif_s', {}).items(): + vif_s_config = dict_merge(default_vif_values, vif_s_config) + for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): + vif_c_config = dict_merge(default_vif_values, vif_c_config) + + # Check vif, vif-s/vif-c VLAN interfaces for removal + dict = get_removed_vlans(config, dict) + + return dict - if not conf.exists(section): - return ifdict - - effect = conf.list_effective_nodes(section) - active = conf.list_nodes(section) - - # the section to parse for vlan - sections = [] - - # determine which interfaces to add or remove based on disable state - if disabled == disable.both: - # was and is still disabled - ifdict[f'{key}_remove'] = [] - elif disabled == disable.now: - # it is now disable but was not before - ifdict[f'{key}_remove'] = effect - elif disabled == disable.was: - # it was disable but not anymore - ifdict[f'{key}_remove'] = [] - sections = active - else: - # normal change - # get interfaces (currently effective) - to determine which - # interface is no longer present and needs to be removed - ifdict[f'{key}_remove'] = list_diff(effect, active) - sections = active - - current_level = conf.get_level() - - # add each section, the key must already exists - for s in sections: - # set config level to vif interface - conf.set_level(current_level + [section, s]) - # add the vlan config as a key (vlan id) - value (config) pair - ifdict[key][s] = vlan_to_dict(conf) - - # re-set configuration level to leave things as found - conf.set_level(current_level) - - return ifdict - - -def vlan_to_dict(conf, default=vlan_default): - vlan, disabled = intf_to_dict(conf, default) - - # if this is a not within vif-s node, we are done - if conf.get_level()[-2] != 'vif-s': - return vlan - - # ethertype is mandatory on vif-s nodes and only exists here! - # ethertype uses a default of 0x88A8 - tmp = '0x88A8' - if conf.exists('ethertype'): - tmp = conf.return_value('ethertype') - vlan['ethertype'] = get_ethertype(tmp) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c') - - return vlan diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py new file mode 100644 index 000000000..b79893507 --- /dev/null +++ b/python/vyos/configdiff.py @@ -0,0 +1,249 @@ +# 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/>. + +from enum import IntFlag, auto + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.xml import defaults + +class ConfigDiffError(Exception): + """ + Raised on config dict access errors, for example, calling get_value on + a non-leaf node. + """ + pass + +def enum_to_key(e): + return e.name.lower() + +class Diff(IntFlag): + MERGE = auto() + DELETE = auto() + ADD = auto() + STABLE = auto() + +requires_effective = [enum_to_key(Diff.DELETE)] +target_defaults = [enum_to_key(Diff.MERGE)] + +def _key_sets_from_dicts(session_dict, effective_dict): + session_keys = list(session_dict) + effective_keys = list(effective_dict) + + ret = {} + stable_keys = [k for k in session_keys if k in effective_keys] + + ret[enum_to_key(Diff.MERGE)] = session_keys + ret[enum_to_key(Diff.DELETE)] = [k for k in effective_keys if k not in stable_keys] + ret[enum_to_key(Diff.ADD)] = [k for k in session_keys if k not in stable_keys] + ret[enum_to_key(Diff.STABLE)] = stable_keys + + return ret + +def _dict_from_key_set(key_set, d): + # This will always be applied to a key_set obtained from a get_sub_dict, + # hence there is no possibility of KeyError, as get_sub_dict guarantees + # a return type of dict + ret = {k: d[k] for k in key_set} + + return ret + +def get_config_diff(config, key_mangling=None): + """ + Check type and return ConfigDiff instance. + """ + if not config or not isinstance(config, Config): + raise TypeError("argument must me a Config instance") + if key_mangling and not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + + return ConfigDiff(config, key_mangling) + +class ConfigDiff(object): + """ + The class of config changes as represented by comparison between the + session config dict and the effective config dict. + """ + def __init__(self, config, key_mangling=None): + self._level = config.get_level() + self._session_config_dict = config.get_cached_dict() + self._effective_config_dict = config.get_cached_dict(effective=True) + self._key_mangling = key_mangling + + # mirrored from Config; allow path arguments relative to level + def _make_path(self, path): + if isinstance(path, str): + path = path.split() + elif isinstance(path, list): + pass + else: + raise TypeError("Path must be a whitespace-separated string or a list") + + ret = self._level + path + return ret + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config dict path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``get_value("name-server")`` is equivalent to calling + ``get_value("system name-server")`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + if isinstance(path, str): + if path: + self._level = path.split() + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + ret = self._level.copy() + return ret + + def _mangle_dict_keys(self, config_dict): + config_dict = mangle_dict_keys(config_dict, self._key_mangling[0], + self._key_mangling[1]) + return config_dict + + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, under path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, + self._make_path(path), get_first_key=True) + effective_dict = get_sub_dict(self._effective_config_dict, + self._make_path(path), get_first_key=True) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, at path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_value_diff(self, path=[]): + """ + Args: + path (str|list): config path + + Returns: (new, old) tuple of values in session config/effective config + """ + # one should properly use is_leaf as check; for the moment we will + # deduce from type, which will not catch call on non-leaf node if None + new_value_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + old_value_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + new_value = None + old_value = None + if new_value_dict: + new_value = next(iter(new_value_dict.values())) + if old_value_dict: + old_value = next(iter(old_value_dict.values())) + + if new_value and isinstance(new_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + if old_value and isinstance(old_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + + return new_value, old_value diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index f2524b37e..0994fd974 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -26,7 +26,7 @@ DISCARD = '/opt/vyatta/sbin/my_discard' SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl'] -INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image'] +INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url'] REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py new file mode 100644 index 000000000..50222e385 --- /dev/null +++ b/python/vyos/configsource.py @@ -0,0 +1,318 @@ + +# 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 re +import subprocess + +from vyos.configtree import ConfigTree + +class VyOSError(Exception): + """ + Raised on config access errors. + """ + pass + +class ConfigSourceError(Exception): + ''' + Raised on error in ConfigSource subclass init. + ''' + pass + +class ConfigSource: + def __init__(self): + self._running_config: ConfigTree = None + self._session_config: ConfigTree = None + + def get_configtree_tuple(self): + return self._running_config, self._session_config + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + +class ConfigSourceSession(ConfigSource): + def __init__(self, session_env=None): + super().__init__() + self._cli_shell_api = "/bin/cli-shell-api" + self._level = [] + if session_env: + self.__session_env = session_env + else: + self.__session_env = None + + # Running config can be obtained either from op or conf mode, it always succeeds + # once the config system is initialized during boot; + # before initialization, set to empty string + if os.path.isfile('/tmp/vyos-config-status'): + try: + running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + running_config_text = '' + else: + running_config_text = '' + + # Session config ("active") only exists in conf mode. + # In op mode, we'll just use the same running config for both active and session configs. + if self.in_session(): + try: + session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + session_config_text = '' + else: + session_config_text = running_config_text + + if running_config_text: + self._running_config = ConfigTree(running_config_text) + else: + self._running_config = None + + if session_config_text: + self._session_config = ConfigTree(session_config_text) + else: + self._session_config = None + + def _make_command(self, op, path): + args = path.split() + cmd = [self._cli_shell_api, op] + args + return cmd + + def _run(self, cmd): + if self.__session_env: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) + else: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + out = p.stdout.read() + p.wait() + p.communicate() + if p.returncode != 0: + raise VyOSError() + else: + return out.decode('ascii') + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config tree path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``exists("name-server")`` is equivalent to calling + ``exists("system name-server"`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + # Make sure there's always a space between default path (level) + # and path supplied as method argument + # XXX: for small strings in-place concatenation is not a problem + if isinstance(path, str): + if path: + self._level = re.split(r'\s+', path) + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + try: + self._run(self._make_command('sessionChanged', '')) + return True + except VyOSError: + return False + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + try: + self._run(self._make_command('inSession', '')) + return True + except VyOSError: + return False + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + + # FIXUP: by default, showConfig will give you a diff + # if there are uncommitted changes. + # The config parser obviously cannot work with diffs, + # so we need to supress diff production using appropriate + # options for getting either running (active) + # or proposed (working) config. + if effective: + path = ['--show-active-only'] + path + else: + path = ['--show-working-only'] + path + + if isinstance(path, list): + path = " ".join(path) + try: + out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env + return out + except VyOSError: + self.__session_env = save_env + return(default) + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isMulti', path)) + return True + except VyOSError: + return False + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isTag', path)) + return True + except VyOSError: + return False + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isLeaf', path)) + return True + except VyOSError: + return False + +class ConfigSourceString(ConfigSource): + def __init__(self, running_config_text=None, session_config_text=None): + super().__init__() + + try: + self._running_config = ConfigTree(running_config_text) if running_config_text else None + self._session_config = ConfigTree(session_config_text) if session_config_text else None + except ValueError: + raise ConfigSourceError(f"Init error in {type(self)}") diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 32129a048..bb590a514 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -41,14 +41,14 @@ def verify_vrf(config): def verify_address(config): """ - Common helper function used by interface implementations to - perform recurring validation of IP address assignmenr - when interface also is part of a bridge. + Common helper function used by interface implementations to perform + recurring validation of IP address assignment when interface is part + of a bridge or bond. """ if {'is_bridge_member', 'address'} <= set(config): raise ConfigError( - f'Cannot assign address to interface "{ifname}" as it is a ' - f'member of bridge "{is_bridge_member}"!'.format(**config)) + 'Cannot assign address to interface "{ifname}" as it is a ' + 'member of bridge "{is_bridge_member}"!'.format(**config)) def verify_bridge_delete(config): @@ -62,6 +62,15 @@ def verify_bridge_delete(config): 'Interface "{ifname}" cannot be deleted as it is a ' 'member of bridge "{is_bridge_member}"!'.format(**config)) +def verify_interface_exists(config): + """ + Common helper function used by interface implementations to perform + recurring validation if an interface actually exists. + """ + from netifaces import interfaces + if not config['ifname'] in interfaces(): + raise ConfigError('Interface "{ifname}" does not exist!' + .format(**config)) def verify_source_interface(config): """ @@ -70,9 +79,43 @@ def verify_source_interface(config): required by e.g. peth/MACvlan, MACsec ... """ from netifaces import interfaces - if not 'source_interface' in config.keys(): + if 'source_interface' not in config: raise ConfigError('Physical source-interface required for ' 'interface "{ifname}"'.format(**config)) - if not config['source_interface'] in interfaces(): - raise ConfigError(f'Source interface {source_interface} does not ' - f'exist'.format(**config)) + if config['source_interface'] not in interfaces(): + raise ConfigError('Source interface {source_interface} does not ' + 'exist'.format(**config)) + +def verify_dhcpv6(config): + """ + Common helper function used by interface implementations to perform + recurring validation of DHCPv6 options which are mutually exclusive. + """ + if {'parameters_only', 'temporary'} <= set(config.get('dhcpv6_options', {})): + raise ConfigError('DHCPv6 temporary and parameters-only options ' + 'are mutually exclusive!') + +def verify_vlan_config(config): + """ + Common helper function used by interface implementations to perform + recurring validation of interface VLANs + """ + # 802.1q VLANs + for vlan in config.get('vif', {}).keys(): + vlan = config['vif'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + # 802.1ad (Q-in-Q) VLANs + for vlan in config.get('vif_s', {}).keys(): + vlan = config['vif_s'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + for vlan in config.get('vif_s', {}).get('vif_c', {}).keys(): + vlan = config['vif_c'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 47dd4ff34..193cea321 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -14,14 +14,15 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import jmespath from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN +from vyos.util import cmd from vyos.validate import assert_list from vyos.validate import assert_positive - @Interface.register @VLAN.enable class BondIf(Interface): @@ -179,7 +180,13 @@ class BondIf(Interface): >>> BondIf('bond0').get_arp_ip_target() '192.0.2.1' """ - return self.get_interface('bond_arp_ip_target') + # As this function might also be called from update() of a VLAN interface + # we must check if the bond_arp_ip_target retrieval worked or not - as this + # can not be set for a bond vif interface + try: + return self.get_interface('bond_arp_ip_target') + except FileNotFoundError: + return '' def set_arp_ip_target(self, target): """ @@ -209,11 +216,31 @@ class BondIf(Interface): >>> BondIf('bond0').add_port('eth0') >>> BondIf('bond0').add_port('eth1') """ - # An interface can only be added to a bond if it is in 'down' state. If - # interface is in 'up' state, the following Kernel error will be thrown: - # bond0: eth1 is up - this may be due to an out of date ifenslave. - Interface(interface).set_admin_state('down') - return self.set_interface('bond_add_port', f'+{interface}') + + # From drivers/net/bonding/bond_main.c: + # ... + # bond_set_slave_link_state(new_slave, + # BOND_LINK_UP, + # BOND_SLAVE_NOTIFY_NOW); + # ... + # + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the CLI will tell us! + + # Physical interface must be in admin down state before they can be + # enslaved. If this is not the case an error will be shown: + # bond0: eth0 is up - this may be due to an out of date ifenslave + slave = Interface(interface) + slave_state = slave.get_admin_state() + if slave_state == 'up': + slave.set_admin_state('down') + + ret = self.set_interface('bond_add_port', f'+{interface}') + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the LI is configured for - thus we place the interface in its + # desired state + slave.set_admin_state(slave_state) + return ret def del_port(self, interface): """ @@ -277,3 +304,80 @@ class BondIf(Interface): >>> BondIf('bond0').set_mode('802.3ad') """ return self.set_interface('bond_mode', mode) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # use ref-counting function to place an interface into admin down state. + # set_admin_state_up() must be called the same amount of times else the + # interface won't come up. This can/should be used to prevent link flapping + # when changing interface parameters require the interface to be down. + # We will disable it once before reconfiguration and enable it afterwards. + if 'shutdown_required' in config: + self.set_admin_state('down') + + # call base class first + super().update(config) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = jmespath.search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + + # Bonding transmit hash policy + value = config.get('hash_policy') + if value: self.set_hash_policy(value) + + # Some interface options can only be changed if the interface is + # administratively down + if self.get_admin_state() == 'down': + # Delete bond member port(s) + for interface in self.get_slaves(): + self.del_port(interface) + + # Bonding policy/mode + value = config.get('mode') + if value: self.set_mode(value) + + # Add (enslave) interfaces to bond + value = jmespath.search('member.interface', config) + if value: + for interface in value: + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + self.add_port(interface) + + # Primary device interface - must be set after 'mode' + value = config.get('primary') + if value: self.set_primary(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 44b92c1db..466e6b682 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -13,12 +13,13 @@ # 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 jmespath from vyos.ifconfig.interface import Interface - +from vyos.ifconfig.stp import STP from vyos.validate import assert_boolean from vyos.validate import assert_positive - +from vyos.util import cmd @Interface.register class BridgeIf(Interface): @@ -187,3 +188,77 @@ class BridgeIf(Interface): >>> BridgeIf('br0').del_port('eth1') """ return self.set_interface('del_port', interface) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Set ageing time + value = config.get('aging') + self.set_ageing_time(value) + + # set bridge forward delay + value = config.get('forwarding_delay') + self.set_forward_delay(value) + + # set hello time + value = config.get('hello_time') + self.set_hello_time(value) + + # set max message age + value = config.get('max_age') + self.set_max_age(value) + + # set bridge priority + value = config.get('priority') + self.set_priority(value) + + # enable/disable spanning tree + value = '1' if 'stp' in config else '0' + self.set_stp(value) + + # enable or disable IGMP querier + tmp = jmespath.search('igmp.querier', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_querier(value) + + # remove interface from bridge + tmp = jmespath.search('member.interface_remove', config) + if tmp: + for member in tmp: + self.del_port(member) + + STPBridgeIf = STP.enable(BridgeIf) + tmp = jmespath.search('member.interface', config) + if tmp: + for interface, interface_config in tmp.items(): + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + # enslave interface port to bridge + self.add_port(interface) + + tmp = STPBridgeIf(interface) + # set bridge port path cost + value = interface_config.get('cost') + tmp.set_path_cost(value) + + # set bridge port path priority + value = interface_config.get('priority') + tmp.set_path_priority(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py deleted file mode 100644 index a8b9a2a87..000000000 --- a/python/vyos/ifconfig/dhcp.py +++ /dev/null @@ -1,136 +0,0 @@ -# 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 - -from vyos.dicts import FixedDict -from vyos.ifconfig.control import Control -from vyos.template import render - -class _DHCPv4 (Control): - def __init__(self, ifname): - super().__init__() - config_base = r'/var/lib/dhcp/dhclient_' - self.options = FixedDict(**{ - 'ifname': ifname, - 'hostname': '', - 'client_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? - def set(self): - """ - Configure interface as DHCP client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> 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.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) - render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - - return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) - - def delete(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.dhcp.v4.delete() - """ - if not os.path.isfile(self.options['pid_file']): - self._debug_msg('No DHCP client PID found') - return None - - self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) - - # cleanup old config files - 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 (Control): - def __init__(self, ifname): - super().__init__() - self.options = FixedDict(**{ - 'ifname': ifname, - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'dhcpv6_pd_interfaces': [], - 'dhcpv6_pd_length': '' - }) - self._conf_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' - - def set(self): - """ - Configure interface as DHCPv6 client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.dhcp.v6.set() - """ - - # better save then sorry .. should be checked in interface script - # but if you missed it we are safe! - if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']: - raise Exception( - 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - - render(self._conf_file, 'dhcp-client/ipv6.tmpl', self.options, trim_blocks=True) - return self._cmd('systemctl restart dhcp6c@{ifname}.service'.format(**self.options)) - - def delete(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.dhcp.v6.delete() - """ - self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options)) - - # cleanup old config files - if os.path.isfile(self._conf_file): - os.remove(self._conf_file) - - -class DHCP(object): - def __init__(self, ifname): - self.v4 = _DHCPv4(ifname) - self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py index 404c490c7..43614cd1c 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -35,3 +35,22 @@ class DummyIf(Interface): 'prefixes': ['dum', ], }, } + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 5b18926c9..b2f701e00 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -15,13 +15,13 @@ import os import re +import jmespath from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN from vyos.validate import assert_list from vyos.util import run - @Interface.register @VLAN.enable class EthernetIf(Interface): @@ -252,3 +252,58 @@ class EthernetIf(Interface): >>> i.set_udp_offload('on') """ return self.set_interface('ufo', state) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # disable ethernet flow control (pause frames) + value = 'off' if 'disable_flow_control' in config.keys() else 'on' + self.set_flow_control(value) + + # GRO (generic receive offload) + tmp = jmespath.search('offload_options.generic_receive', config) + value = tmp if (tmp != None) else 'off' + self.set_gro(value) + + # GSO (generic segmentation offload) + tmp = jmespath.search('offload_options.generic_segmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_gso(value) + + # scatter-gather option + tmp = jmespath.search('offload_options.scatter_gather', config) + value = tmp if (tmp != None) else 'off' + self.set_sg(value) + + # TSO (TCP segmentation offloading) + tmp = jmespath.search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_tso(value) + + # UDP fragmentation offloading + tmp = jmespath.search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_ufo(value) + + # Set physical interface speed and duplex + if {'speed', 'duplex'} <= set(config): + speed = config.get('speed') + duplex = config.get('duplex') + self.set_speed_duplex(speed, duplex) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 1819ffc82..36f258301 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -16,7 +16,10 @@ import os import re import json +import jmespath + from copy import deepcopy +from glob import glob from ipaddress import IPv4Network from ipaddress import IPv6Address @@ -28,6 +31,8 @@ from netifaces import AF_INET6 from vyos import ConfigError from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.template import render from vyos.util import mac2eui64 from vyos.validate import is_ipv4 from vyos.validate import is_ipv6 @@ -40,11 +45,17 @@ from vyos.validate import assert_positive from vyos.validate import assert_range from vyos.ifconfig.control import Control -from vyos.ifconfig.dhcp import DHCP from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational from vyos.ifconfig import Section +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) class Interface(Control): # This is the class which will be used to create @@ -72,8 +83,12 @@ class Interface(Control): _command_get = { 'admin_state': { 'shellcmd': 'ip -json link show dev {ifname}', - 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down', - } + 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', + }, + 'vlan_protocol': { + 'shellcmd': 'ip -json -details link show dev {ifname}', + 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), + }, } _command_set = { @@ -197,11 +212,11 @@ class Interface(Control): # make sure the ifname is the first argument and not from the dict self.config['ifname'] = ifname + self._admin_state_down_cnt = 0 # we must have updated config before initialising the Interface super().__init__(**kargs) self.ifname = ifname - self.dhcp = DHCP(ifname) if not self.exists(ifname): # Any instance of Interface, such as Interface('eth0') @@ -323,6 +338,10 @@ class Interface(Control): self.set_interface('mac', mac) + # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion + if prev_state == 'up': + self.set_admin_state('up') + def set_vrf(self, vrf=''): """ Add/Remove interface from given VRF instance. @@ -539,6 +558,17 @@ class Interface(Control): """ self.set_interface('alias', ifalias) + def get_vlan_protocol(self): + """ + Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0.10').get_vlan_protocol() + '802.1Q' + """ + return self.get_interface('vlan_protocol') + def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' @@ -560,7 +590,24 @@ class Interface(Control): >>> Interface('eth0').get_admin_state() 'down' """ - return self.set_interface('admin_state', state) + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + if self.get_vlan_protocol(): + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + if state == 'up': + self._admin_state_down_cnt -= 1 + if self._admin_state_down_cnt < 1: + return self.set_interface('admin_state', state) + else: + self._admin_state_down_cnt += 1 + return self.set_interface('admin_state', state) def set_proxy_arp(self, enable): """ @@ -663,21 +710,24 @@ class Interface(Control): if addr in self._addr: return False + addr_is_v4 = is_ipv4(addr) + # we can't have both DHCP and static IPv4 addresses assigned for a in self._addr: if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or - ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ): + ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): raise ConfigError(( "Can't configure both static IPv4 and DHCP address " "on the same interface")) # add to interface if addr == 'dhcp': - self.dhcp.v4.set() + self.set_dhcp(True) elif addr == 'dhcpv6': - self.dhcp.v6.set() + self.set_dhcpv6(True) elif not is_intf_addr_assigned(self.ifname, addr): - self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"') + self._cmd(f'ip addr add "{addr}" ' + f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') else: return False @@ -713,9 +763,9 @@ class Interface(Control): # remove from interface if addr == 'dhcp': - self.dhcp.v4.delete() + self.set_dhcp(False) elif addr == 'dhcpv6': - self.dhcp.v6.delete() + self.set_dhcpv6(False) elif is_intf_addr_assigned(self.ifname, addr): self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') else: @@ -734,8 +784,8 @@ class Interface(Control): Will raise an exception on error. """ # stop DHCP(v6) if running - self.dhcp.v4.delete() - self.dhcp.v6.delete() + self.set_dhcp(False) + self.set_dhcpv6(False) # flush all addresses self._cmd(f'ip addr flush dev "{self.ifname}"') @@ -759,21 +809,95 @@ class Interface(Control): return True + def set_dhcp(self, enable): + """ + Enable/Disable DHCP client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_base = r'/var/lib/dhcp/dhclient' + config_file = f'{config_base}_{ifname}.conf' + options_file = f'{config_base}_{ifname}.options' + pid_file = f'{config_base}_{ifname}.pid' + lease_file = f'{config_base}_{ifname}.leases' + + if enable and 'disable' not in self._config: + if jmespath.search('dhcp_options.host_name', self._config) == None: + # read configured system hostname. + # maybe change to vyos hostd client ??? + hostname = 'vyos' + with open('/etc/hostname', 'r') as f: + hostname = f.read().rstrip('\n') + tmp = {'dhcp_options' : { 'host_name' : hostname}} + self._config = dict_merge(tmp, self._config) + + render(options_file, 'dhcp-client/daemon-options.tmpl', + self._config, trim_blocks=True) + render(config_file, 'dhcp-client/ipv4.tmpl', + self._config, trim_blocks=True) + + # 'up' check is mandatory b/c even if the interface is A/D, as soon as + # the DHCP client is started the interface will be placed in u/u state. + # This is not what we intended to do when disabling an interface. + return self._cmd(f'systemctl restart dhclient@{ifname}.service') + else: + self._cmd(f'systemctl stop dhclient@{ifname}.service') + + # cleanup old config files + for file in [config_file, options_file, pid_file, lease_file]: + if os.path.isfile(file): + os.remove(file) + + + def set_dhcpv6(self, enable): + """ + Enable/Disable DHCPv6 client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + + if enable and 'disable' not in self._config: + render(config_file, 'dhcp-client/ipv6.tmpl', + self._config, trim_blocks=True) + + # We must ignore any return codes. This is required to enable DHCPv6-PD + # for interfaces which are yet not up and running. + return self._popen(f'systemctl restart dhcp6c@{ifname}.service') + else: + self._popen(f'systemctl stop dhcp6c@{ifname}.service') + + if os.path.isfile(config_file): + os.remove(config_file) + + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface. """ + # Cache the configuration - it will be reused inside e.g. DHCP handler + # XXX: maybe pass the option via __init__ in the future and rename this + # method to apply()? + self._config = config + # Update interface description - self.set_alias(config.get('description', None)) + self.set_alias(config.get('description', '')) + + # Ignore link state changes + value = '2' if 'disable_link_detect' in config else '1' + self.set_link_detect(value) # Configure assigned interface IP addresses. No longer # configured addresses will be removed first new_addr = config.get('address', []) - # XXX workaround for T2636, convert IP address string to a list - # with one element + # XXX: T2636 workaround: convert string to a list with one element if isinstance(new_addr, str): new_addr = [new_addr] @@ -789,10 +913,140 @@ class Interface(Control): # There are some items in the configuration which can only be applied # if this instance is not bound to a bridge. This should be checked # by the caller but better save then sorry! - if not config.get('is_bridge_member', False): - # Bind interface instance into VRF + if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): + # Bind interface to given VRF or unbind it if vrf node is not set. + # unbinding will call 'ip link set dev eth0 nomaster' which will + # also drop the interface out of a bridge or bond - thus this is + # checked before self.set_vrf(config.get('vrf', '')) - # Interface administrative state - state = 'down' if 'disable' in config.keys() else 'up' - self.set_admin_state(state) + # Configure ARP cache timeout in milliseconds - has default value + tmp = jmespath.search('ip.arp_cache_timeout', config) + value = tmp if (tmp != None) else '30' + self.set_arp_cache_tmo(value) + + # Configure ARP filter configuration + tmp = jmespath.search('ip.disable_arp_filter', config) + value = '0' if (tmp != None) else '1' + self.set_arp_filter(value) + + # Configure ARP accept + tmp = jmespath.search('ip.enable_arp_accept', config) + value = '1' if (tmp != None) else '0' + self.set_arp_accept(value) + + # Configure ARP announce + tmp = jmespath.search('ip.enable_arp_announce', config) + value = '1' if (tmp != None) else '0' + self.set_arp_announce(value) + + # Configure ARP ignore + tmp = jmespath.search('ip.enable_arp_ignore', config) + value = '1' if (tmp != None) else '0' + self.set_arp_ignore(value) + + # Enable proxy-arp on this interface + tmp = jmespath.search('ip.enable_proxy_arp', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp(value) + + # Enable private VLAN proxy ARP on this interface + tmp = jmespath.search('ip.proxy_arp_pvlan', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp_pvlan(value) + + # IPv6 forwarding + tmp = jmespath.search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + self.set_ipv6_forwarding(value) + + # IPv6 router advertisements + tmp = jmespath.search('ipv6.address.autoconf', config) + value = '2' if (tmp != None) else '1' + if 'dhcpv6' in new_addr: + value = '2' + self.set_ipv6_accept_ra(value) + + # IPv6 address autoconfiguration + tmp = jmespath.search('ipv6.address.autoconf', config) + value = '1' if (tmp != None) else '0' + self.set_ipv6_autoconf(value) + + # IPv6 Duplicate Address Detection (DAD) tries + tmp = jmespath.search('ipv6.dup_addr_detect_transmits', config) + value = tmp if (tmp != None) else '1' + self.set_ipv6_dad_messages(value) + + # MTU - Maximum Transfer Unit + if 'mtu' in config: + self.set_mtu(config.get('mtu')) + + # Delete old IPv6 EUI64 addresses before changing MAC + tmp = jmespath.search('ipv6.address.eui64_old', config) + if tmp: + for addr in tmp: + self.del_ipv6_eui64_address(addr) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed. Skip if bond member. + if 'is_bond_member' not in config: + mac = config.get('hw_id') + if 'mac' in config: + mac = config.get('mac') + if mac: + self.set_mac(mac) + + # Manage IPv6 link-local addresses + tmp = jmespath.search('ipv6.address.no_default_link_local', config) + # we must check explicitly for None type as if the key is set we will + # get an empty dict (<class 'dict'>) + if tmp is not None: + self.del_ipv6_eui64_address('fe80::/64') + else: + self.add_ipv6_eui64_address('fe80::/64') + + # Add IPv6 EUI-based addresses + tmp = jmespath.search('ipv6.address.eui64', config) + if tmp: + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(tmp, str): + tmp = [tmp] + for addr in tmp: + self.add_ipv6_eui64_address(addr) + + # re-add ourselves to any bridge we might have fallen out of + if 'is_bridge_member' in config: + bridge = config.get('is_bridge_member') + self.add_to_bridge(bridge) + + # remove no longer required 802.1ad (Q-in-Q VLANs) + for vif_s_id in config.get('vif_s_remove', {}): + self.del_vlan(vif_s_id) + + # create/update 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] + for vif_s_id, vif_s in config.get('vif_s', {}).items(): + tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) + s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) + vif_s['ifname'] = f'{ifname}.{vif_s_id}' + s_vlan.update(vif_s) + + # remove no longer required client VLAN (vif-c) + for vif_c_id in vif_s.get('vif_c_remove', {}): + s_vlan.del_vlan(vif_c_id) + + # create/update client VLAN (vif-c) interface + for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): + c_vlan = s_vlan.add_vlan(vif_c_id) + vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' + c_vlan.update(vif_c) + + # remove no longer required 802.1q VLAN interfaces + for vif_id in config.get('vif_remove', {}): + self.del_vlan(vif_id) + + # create/update 802.1q VLAN interfaces + for vif_id, vif in config.get('vif', {}).items(): + vlan = self.add_vlan(vif_id) + vif['ifname'] = f'{ifname}.{vif_id}' + vlan.update(vif) diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index 7ebd13b54..2b4ebfdcc 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -75,5 +75,15 @@ class LoopbackIf(Interface): # Update IP address entry in our dictionary config.update({'address' : addr}) - # now call the regular function from within our base class + # call base class super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index ea8c9807e..6f570d162 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -71,3 +71,22 @@ class MACsecIf(Interface): 'source_interface': '', } return config + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index b5481f4a7..b068ce873 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -68,3 +68,22 @@ class MACVLANIf(Interface): >> dict = MACVLANIf().get_config() """ return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 973b4ef05..0dddab7b7 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -47,8 +47,8 @@ class VXLANIf(Interface): 'port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port 'remote': '', - 'src_address': '', - 'src_interface': '', + 'source_address': '', + 'source_interface': '', 'vni': 0 } definition = { @@ -60,29 +60,29 @@ class VXLANIf(Interface): } } options = Interface.options + \ - ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] + ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address'] mapping = { 'ifname': 'add', 'vni': 'id', 'port': 'dstport', - 'src_address': 'local', - 'src_interface': 'dev', + 'source_address': 'local', + 'source_interface': 'dev', } def _create(self): cmdline = ['ifname', 'type', 'vni', 'port'] - if self.config['src_address']: - cmdline.append('src_address') + if self.config['source_address']: + cmdline.append('source_address') if self.config['remote']: cmdline.append('remote') - if self.config['group'] or self.config['src_interface']: - if self.config['group'] and self.config['src_interface']: + if self.config['group'] or self.config['source_interface']: + if self.config['group'] and self.config['source_interface']: cmdline.append('group') - cmdline.append('src_interface') + cmdline.append('source_interface') else: ifname = self.config['ifname'] raise ConfigError( diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 62ca57ca2..fad4ef282 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -24,7 +24,7 @@ from hurry.filesize import alternative from vyos.config import Config from vyos.ifconfig import Interface from vyos.ifconfig import Operational - +from vyos.validate import is_ipv6 class WireGuardOperational(Operational): def _dump(self): @@ -169,65 +169,79 @@ class WireGuardIf(Interface): ['port', 'private_key', 'pubkey', 'psk', 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] - """ - Wireguard interface class, contains a comnfig dictionary since - wireguard VPN is being comnfigured via the wg command rather than - writing the config into a file. Otherwise if a pre-shared key is used - (symetric enryption key), it would we exposed within multiple files. - Currently it's only within the config.boot if the config was saved. - - Example: - >>> from vyos.ifconfig import WireGuardIf as wg_if - >>> wg_intfc = wg_if("wg01") - >>> print (wg_intfc.wg_config) - {'private_key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, - 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - >>> wg_intfc.wg_config['keepalive'] = 100 - >>> print (wg_intfc.wg_config) - {'private_key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, - 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - """ - - def update(self): - if not self.config['private_key']: - raise ValueError("private key required") - else: - # fmask permission check? - pass - - cmd = 'wg set {ifname}'.format(**self.config) - cmd += ' listen-port {port}'.format(**self.config) - cmd += ' fwmark "{fwmark}" '.format(**self.config) - cmd += ' private-key {private_key}'.format(**self.config) - cmd += ' peer {pubkey}'.format(**self.config) - cmd += ' persistent-keepalive {keepalive}'.format(**self.config) - # allowed-ips must be properly quoted else the interface can't be properly - # created as the wg utility will tread multiple IP addresses as command - # parameters - cmd += ' allowed-ips "{}"'.format(','.join(self.config['allowed-ips'])) - - if self.config['endpoint']: - cmd += ' endpoint "{endpoint}"'.format(**self.config) - - psk_file = '' - if self.config['psk']: - psk_file = '/tmp/{ifname}.psk'.format(**self.config) - with open(psk_file, 'w') as f: - f.write(self.config['psk']) + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # remove no longer associated peers first + if 'peer_remove' in config: + for tmp in config['peer_remove']: + peer = config['peer_remove'][tmp] + peer['ifname'] = config['ifname'] + + cmd = 'wg set {ifname} peer {pubkey} remove' + self._cmd(cmd.format(**peer)) + + # Wireguard base command is identical for every peer + base_cmd = 'wg set {ifname} private-key {private_key}' + if 'port' in config: + base_cmd += ' listen-port {port}' + if 'fwmark' in config: + base_cmd += ' fwmark {fwmark}' + + base_cmd = base_cmd.format(**config) + + for tmp in config['peer']: + peer = config['peer'][tmp] + + # start of with a fresh 'wg' command + cmd = base_cmd + ' peer {pubkey}' + + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + no_psk_file = '/dev/null' + psk_file = no_psk_file + if 'preshared_key' in peer: + psk_file = '/tmp/tmp.wireguard.psk' + with open(psk_file, 'w') as f: + f.write(peer['preshared_key']) cmd += f' preshared-key {psk_file}' - self._cmd(cmd) - - # PSK key file is not required to be stored persistently as its backed by CLI - if os.path.exists(psk_file): - os.remove(psk_file) - - def remove_peer(self, peerkey): - """ - Remove a peer of an interface, peers are identified by their public key. - Giving it a readable name is a vyos feature, to remove a peer the pubkey - and the interface is needed, to remove the entry. - """ - cmd = "wg set {0} peer {1} remove".format( - self.config['ifname'], str(peerkey)) - return self._cmd(cmd) + # Persistent keepalive is optional + if 'persistent_keepalive'in peer: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer['allowed_ips'], str): + peer['allowed_ips'] = [peer['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer['allowed_ips']) + + # Endpoint configuration is optional + if {'address', 'port'} <= set(peer): + if is_ipv6(config['address']): + cmd += ' endpoint [{address}]:{port}' + else: + cmd += ' endpoint {address}:{port}' + + self._cmd(cmd.format(**peer)) + + # PSK key file is not required to be stored persistently as its backed by CLI + if psk_file != no_psk_file and os.path.exists(psk_file): + os.remove(psk_file) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py deleted file mode 100644 index 442cb0db8..000000000 --- a/python/vyos/ifconfig_vlan.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright 2019-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/>. - -from netifaces import interfaces -from vyos import ConfigError - -def apply_all_vlans(intf, intfconfig): - """ - Function applies all VLANs to the passed interface. - - intf: object of Interface class - intfconfig: dict with interface configuration - """ - # remove no longer required service VLAN interfaces (vif-s) - for vif_s in intfconfig['vif_s_remove']: - intf.del_vlan(vif_s) - - # create service VLAN interfaces (vif-s) - for vif_s_id, vif_s in intfconfig['vif_s'].items(): - s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype']) - apply_vlan_config(s_vlan, vif_s) - - # remove no longer required client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c_remove']: - s_vlan.del_vlan(vif_c) - - # create client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c_id, vif_c in vif_s['vif_c'].items(): - c_vlan = s_vlan.add_vlan(vif_c_id) - apply_vlan_config(c_vlan, vif_c) - - # remove no longer required VLAN interfaces (vif) - for vif in intfconfig['vif_remove']: - intf.del_vlan(vif) - - # create VLAN interfaces (vif) - for vif_id, vif in intfconfig['vif'].items(): - # QoS priority mapping can only be set during interface creation - # so we delete the interface first if required. - if vif['egress_qos_changed'] or vif['ingress_qos_changed']: - try: - # on system bootup the above condition is true but the interface - # does not exists, which throws an exception, but that's legal - intf.del_vlan(vif_id) - except: - pass - - vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) - apply_vlan_config(vlan, vif) - - -def apply_vlan_config(vlan, config): - """ - Generic function to apply a VLAN configuration from a dictionary - to a VLAN interface - """ - - if not vlan.definition['vlan']: - raise TypeError() - - if config['dhcp_client_id']: - vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id'] - - if config['dhcp_hostname']: - vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname'] - - if config['dhcp_vendor_class_id']: - vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id'] - - if config['dhcpv6_prm_only']: - vlan.dhcp.v6.options['dhcpv6_prm_only'] = True - - if config['dhcpv6_temporary']: - vlan.dhcp.v6.options['dhcpv6_temporary'] = True - - if config['dhcpv6_pd_length']: - vlan.dhcp.v6.options['dhcpv6_pd_length'] = config['dhcpv6_pd_length'] - - if config['dhcpv6_pd_interfaces']: - vlan.dhcp.v6.options['dhcpv6_pd_interfaces'] = config['dhcpv6_pd_interfaces'] - - # update interface description used e.g. within SNMP - vlan.set_alias(config['description']) - # ignore link state changes - vlan.set_link_detect(config['disable_link_detect']) - # configure ARP filter configuration - vlan.set_arp_filter(config['ip_disable_arp_filter']) - # configure ARP accept - vlan.set_arp_accept(config['ip_enable_arp_accept']) - # configure ARP announce - vlan.set_arp_announce(config['ip_enable_arp_announce']) - # configure ARP ignore - vlan.set_arp_ignore(config['ip_enable_arp_ignore']) - # configure Proxy ARP - vlan.set_proxy_arp(config['ip_proxy_arp']) - # IPv6 accept RA - vlan.set_ipv6_accept_ra(config['ipv6_accept_ra']) - # IPv6 address autoconfiguration - vlan.set_ipv6_autoconf(config['ipv6_autoconf']) - # IPv6 forwarding - vlan.set_ipv6_forwarding(config['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect']) - # Maximum Transmission Unit (MTU) - vlan.set_mtu(config['mtu']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not config['is_bridge_member']: - vlan.set_vrf(config['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in config['ipv6_eui64_prefix_remove']: - vlan.del_ipv6_eui64_address(addr) - - # Change VLAN interface MAC address - if config['mac']: - vlan.set_mac(config['mac']) - - # Add IPv6 EUI-based addresses - for addr in config['ipv6_eui64_prefix']: - vlan.add_ipv6_eui64_address(addr) - - # enable/disable VLAN interface - if config['disable']: - vlan.set_admin_state('down') - else: - vlan.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - - # re-add ourselves to any bridge we might have fallen out of - if config['is_bridge_member']: - vlan.add_to_bridge(config['is_bridge_member']) - -def verify_vlan_config(config): - """ - Generic function to verify VLAN config consistency. Instead of re- - implementing this function in multiple places use single source \o/ - """ - - # config['vif'] is a dict with ids as keys and config dicts as values - for vif in config['vif'].values(): - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - if ( vif['is_bridge_member'] - and ( vif['address'] - or vif['ipv6_eui64_prefix'] - or vif['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif interface {vif["intf"]} ' - f'which is a member of bridge {vif["is_bridge_member"]}')) - - if vif['vrf']: - if vif['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif["vrf"]}" does not exist') - - if vif['is_bridge_member']: - raise ConfigError(( - f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} ' - f'and bridge {vif["is_bridge_member"]} at the same time!')) - - # e.g. wireless interface has no vif_s support - # thus we bail out eraly. - if 'vif_s' not in config.keys(): - return - - # config['vif_s'] is a dict with ids as keys and config dicts as values - for vif_s_id, vif_s in config['vif_s'].items(): - for vif_id, vif in config['vif'].items(): - if vif_id == vif_s_id: - raise ConfigError(( - f'Cannot use identical ID on vif "{vif["intf"]}" ' - f'and vif-s "{vif_s["intf"]}"')) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError(( - 'DHCPv6 temporary and parameters-only options are mutually ' - 'exclusive!')) - - if ( vif_s['is_bridge_member'] - and ( vif_s['address'] - or vif_s['ipv6_eui64_prefix'] - or vif_s['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif-s interface {vif_s["intf"]} ' - f'which is a member of bridge {vif_s["is_bridge_member"]}')) - - if vif_s['vrf']: - if vif_s['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist') - - if vif_s['is_bridge_member']: - raise ConfigError(( - f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} ' - f'and bridge {vif_s["is_bridge_member"]} at the same time!')) - - # vif_c is a dict with ids as keys and config dicts as values - for vif_c in vif_s['vif_c'].values(): - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError(( - 'DHCPv6 temporary and parameters-only options are ' - 'mutually exclusive!')) - - if ( vif_c['is_bridge_member'] - and ( vif_c['address'] - or vif_c['ipv6_eui64_prefix'] - or vif_c['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif-c interface {vif_c["intf"]} ' - f'which is a member of bridge {vif_c["is_bridge_member"]}')) - - if vif_c['vrf']: - if vif_c['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist') - - if vif_c['is_bridge_member']: - raise ConfigError(( - f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} ' - f'and bridge {vif_c["is_bridge_member"]} at the same time!')) - diff --git a/python/vyos/snmpv3_hashgen.py b/python/vyos/snmpv3_hashgen.py new file mode 100644 index 000000000..324c3274d --- /dev/null +++ b/python/vyos/snmpv3_hashgen.py @@ -0,0 +1,50 @@ +# 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/>. + +# Documentation / Inspiration +# - https://tools.ietf.org/html/rfc3414#appendix-A.3 +# - https://github.com/TheMysteriousX/SNMPv3-Hash-Generator + +key_length = 1048576 + +def random(l): + # os.urandom(8) returns 8 bytes of random data + import os + from binascii import hexlify + return hexlify(os.urandom(l)).decode('utf-8') + +def expand(s, l): + """ repead input string (s) as long as we reach the desired length in bytes """ + from itertools import repeat + reps = l // len(s) + 1 # approximation; worst case: overrun = l + len(s) + return ''.join(list(repeat(s, reps)))[:l].encode('utf-8') + +def plaintext_to_md5(passphrase, engine): + """ Convert input plaintext passphrase to MD5 hashed version usable by net-snmp """ + from hashlib import md5 + tmp = expand(passphrase, key_length) + hash = md5(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return md5(out).digest().hex() + +def plaintext_to_sha1(passphrase, engine): + """ Convert input plaintext passphrase to SHA1hashed version usable by net-snmp """ + from hashlib import sha1 + tmp = expand(passphrase, key_length) + hash = sha1(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return sha1(out).digest().hex() diff --git a/python/vyos/util.py b/python/vyos/util.py index 924df6b3a..7078762df 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -242,7 +242,7 @@ def chown(path, user, group): if not os.path.exists(path): return False - + uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid os.chown(path, uid, gid) @@ -652,3 +652,12 @@ def get_bridge_member_config(conf, br, intf): conf.set_level(old_level) return memberconf + +def check_kmod(k_mod): + """ Common utility function to load required kernel modules on demand """ + if isinstance(k_mod, str): + k_mod = k_mod.split() + for module in k_mod: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 9072c5817..ceeb6888a 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -19,6 +19,7 @@ import netifaces import ipaddress from vyos.util import cmd +from vyos import xml # Important note when you are adding new validation functions: # @@ -278,7 +279,6 @@ def is_member(conf, interface, intftype=None): False -> interface type cannot have members """ ret_val = None - if intftype not in ['bonding', 'bridge', None]: raise ValueError(( f'unknown interface type "{intftype}" or it cannot ' @@ -291,14 +291,14 @@ def is_member(conf, interface, intftype=None): conf.set_level([]) for it in intftype: - base = 'interfaces ' + it + base = ['interfaces', it] for intf in conf.list_nodes(base): - memberintf = f'{base} {intf} member interface' - if conf.is_tag(memberintf): + memberintf = base + [intf, 'member', 'interface'] + if xml.is_tag(memberintf): if interface in conf.list_nodes(memberintf): ret_val = intf break - elif conf.is_leaf(memberintf): + elif xml.is_leaf(memberintf): if ( conf.exists(memberintf) and interface in conf.return_values(memberintf) ): ret_val = intf diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py index 6e0e73b1b..0f914fed2 100644 --- a/python/vyos/xml/__init__.py +++ b/python/vyos/xml/__init__.py @@ -35,6 +35,18 @@ def load_configuration(cache=[]): return xml +# def is_multi(lpath): +# return load_configuration().is_multi(lpath) + + +def is_tag(lpath): + return load_configuration().is_tag(lpath) + + +def is_leaf(lpath, flat=True): + return load_configuration().is_leaf(lpath, flat) + + def defaults(lpath, flat=False): return load_configuration().defaults(lpath, flat) @@ -42,3 +54,6 @@ def defaults(lpath, flat=False): if __name__ == '__main__': print(defaults(['service'], flat=True)) print(defaults(['service'], flat=False)) + + print(is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + print(is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py index 5421007e0..098e64f7e 100644 --- a/python/vyos/xml/definition.py +++ b/python/vyos/xml/definition.py @@ -126,10 +126,12 @@ class XML(dict): elif word: if data_node != kw.plainNode or len(passed) == 1: self.options = [_ for _ in self.tree if _.startswith(word)] + self.options.sort() else: self.options = [] else: self.options = named_options + self.options.sort() self.plain = not is_dataNode @@ -143,6 +145,7 @@ class XML(dict): self.word = '' if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): self.options = [_ for _ in self.tree if not kw.found(_)] + self.options.sort() def checks(self, cmd): # as we move thought the named node twice @@ -248,7 +251,7 @@ class XML(dict): def defaults(self, lpath, flat): d = self[kw.default] for k in lpath: - d = d[k] + d = d.get(k, {}) if not flat: r = {} @@ -300,16 +303,28 @@ class XML(dict): return tree def _get(self, lpath, tag, with_tag=True): - return self._tree(lpath + [tag], with_tag) + tree = self._tree(lpath, with_tag) + if tree is None: + return None + return tree.get(tag, None) def is_multi(self, lpath, with_tag=True): - return self._get(lpath, kw.multi, with_tag) is True + tree = self._get(lpath, kw.multi, with_tag) + if tree is None: + return None + return tree is True def is_tag(self, lpath, with_tag=True): - return self._get(lpath, kw.node, with_tag) == kw.tagNode + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.tagNode def is_leaf(self, lpath, with_tag=True): - return self._get(lpath, kw.node, with_tag) == kw.leafNode + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.leafNode def exists(self, lpath, with_tag=True): return self._get(lpath, kw.node, with_tag) is not None diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py index ac0620d99..ff55151d2 100644 --- a/python/vyos/xml/test_xml.py +++ b/python/vyos/xml/test_xml.py @@ -33,7 +33,7 @@ class TestSearch(TestCase): last = self.xml.traverse("") self.assertEqual(last, '') self.assertEqual(self.xml.inside, []) - self.assertEqual(self.xml.options, ['protocols', 'service', 'system', 'firewall', 'interfaces', 'vpn', 'nat', 'vrf', 'high-availability']) + self.assertEqual(self.xml.options, ['firewall', 'high-availability', 'interfaces', 'nat', 'protocols', 'service', 'system', 'vpn', 'vrf']) self.assertEqual(self.xml.filling, False) self.assertEqual(self.xml.word, last) self.assertEqual(self.xml.check, False) diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 689d19ece..c60b32a1e 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -111,7 +111,7 @@ def get_properties(p): for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: - comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text)) + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 5c7294296..a3e141a00 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -15,151 +15,84 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import fnmatch +from glob import glob +from netifaces import interfaces from sys import exit -from copy import deepcopy from vyos.config import Config -from vyos import ConfigError from vyos.util import call from vyos.template import render - +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/etc/default/udp-broadcast-relay' - -default_config_data = { - 'disabled': False, - 'instances': [] -} +config_file_base = r'/etc/default/udp-broadcast-relay' def get_config(): - relay = deepcopy(default_config_data) conf = Config() base = ['service', 'broadcast-relay'] - if not conf.exists(base): - return None - else: - conf.set_level(base) - - # Service can be disabled by user - if conf.exists('disable'): - relay['disabled'] = True - return relay - - # Parse configuration of each individual instance - if conf.exists('id'): - for id in conf.list_nodes('id'): - conf.set_level(base + ['id', id]) - config = { - 'id': id, - 'disabled': False, - 'address': '', - 'description': '', - 'interfaces': [], - 'port': '' - } - - # Check if individual broadcast relay service is disabled - if conf.exists(['disable']): - config['disabled'] = True - - # Source IP of forwarded packets, if empty original senders address is used - if conf.exists(['address']): - config['address'] = conf.return_value(['address']) - - # A description for each individual broadcast relay service - if conf.exists(['description']): - config['description'] = conf.return_value(['description']) - - # UDP port to listen on for broadcast frames - if conf.exists(['port']): - config['port'] = conf.return_value(['port']) - - # Network interfaces to listen on for broadcast frames to be relayed - if conf.exists(['interface']): - config['interfaces'] = conf.return_values(['interface']) - - relay['instances'].append(config) - + relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return relay def verify(relay): - if relay is None: - return None - - if relay['disabled']: + if not relay or 'disabled' in relay: return None - for r in relay['instances']: + for instance, config in relay.get('id', {}).items(): # we don't have to check this instance when it's disabled - if r['disabled']: + if 'disabled' in config: continue # we certainly require a UDP port to listen to - if not r['port']: - raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id'])) + if 'port' not in config: + raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"') + # if only oone interface is given it's a string -> move to list + if isinstance(config.get('interface', []), str): + config['interface'] = [ config['interface'] ] # Relaying data without two interface is kinda senseless ... - if len(r['interfaces']) < 2: - raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id'])) + if len(config.get('interface', [])) < 2: + raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"') - return None + for interface in config.get('interface', []): + if interface not in interfaces(): + raise ConfigError('Interface "{interface}" does not exist!') + return None def generate(relay): - if relay is None: + if not relay or 'disabled' in relay: return None - config_dir = os.path.dirname(config_file) - config_filename = os.path.basename(config_file) - active_configs = [] - - for config in fnmatch.filter(os.listdir(config_dir), config_filename + '*'): - # determine prefix length to identify service instance - prefix_len = len(config_filename) - active_configs.append(config[prefix_len:]) - - # sort our list - active_configs.sort() + for config in glob(config_file_base + '*'): + os.remove(config) - # delete old configuration files - for id in active_configs[:]: - if os.path.exists(config_file + id): - os.unlink(config_file + id) - - # If the service is disabled, we can bail out here - if relay['disabled']: - print('Warning: UDP broadcast relay service will be deactivated because it is disabled') - return None - - for r in relay['instances']: - # Skip writing instance config when it's disabled - if r['disabled']: + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: continue - # configuration filename contains instance id - file = config_file + str(r['id']) - render(file, 'bcast-relay/udp-broadcast-relay.tmpl', r) + config['instance'] = instance + render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', config) return None def apply(relay): # first stop all running services - call('systemctl stop udp-broadcast-relay@{1..99}.service') + call('systemctl stop udp-broadcast-relay@*.service') - if (relay is None) or relay['disabled']: + if not relay or 'disable' in relay: return None # start only required service instances - for r in relay['instances']: - # Don't start individual instance when it's disabled - if r['disabled']: + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: continue - call('systemctl start udp-broadcast-relay@{0}.service'.format(r['id'])) + + call(f'systemctl start udp-broadcast-relay@{instance}.service') return None diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index f2fa64233..9d66bd434 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -18,20 +18,16 @@ conf-mode script for 'system host-name' and 'system domain-name'. """ -import os import re import sys import copy -import glob -import argparse -import jinja2 import vyos.util import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, call, run, process_named_running +from vyos.util import cmd, call, process_named_running from vyos import airbag airbag.enable() @@ -47,7 +43,9 @@ default_config_data = { hostsd_tag = 'system' -def get_config(conf): +def get_config(): + conf = Config() + hosts = copy.deepcopy(default_config_data) hosts['hostname'] = conf.return_value("system host-name") @@ -77,7 +75,7 @@ def get_config(conf): return hosts -def verify(conf, hosts): +def verify(hosts): if hosts is None: return None @@ -168,9 +166,8 @@ def apply(config): if __name__ == '__main__': try: - conf = Config() - c = get_config(conf) - verify(conf, c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index a16c4e105..3b238f1ea 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -16,41 +16,25 @@ import os -from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data from vyos.config import Config -from vyos.util import call, cmd -from vyos.validate import is_member, has_address_configured +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import BondIf +from vyos.validate import is_member +from vyos.validate import has_address_configured from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'arp_mon_intvl': 0, - 'arp_mon_tgt': [], - 'deleted': False, - 'hash_policy': 'layer2', - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'mode': '802.3ad', - 'member': [], - 'shutdown_required': False, - 'primary': '', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], -} - - def get_bond_mode(mode): if mode == 'round-robin': return 'balance-rr' @@ -67,339 +51,138 @@ def get_bond_mode(mode): elif mode == 'adaptive-load-balance': return 'balance-alb' else: - raise ConfigError('invalid bond mode "{}"'.format(mode)) + raise ConfigError(f'invalid bond mode "{mode}"') def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() + base = ['interfaces', 'bonding'] + bond = get_interface_dict(conf, base) + + # To make our own life easier transfor the list of member interfaces + # into a dictionary - we will use this to add additional information + # later on for wach member + if 'member' in bond and 'interface' in bond['member']: + # first convert it to a list if only one member is given + if isinstance(bond['member']['interface'], str): + bond['member']['interface'] = [bond['member']['interface']] + + tmp={} + for interface in bond['member']['interface']: + tmp.update({interface: {}}) + + bond['member']['interface'] = tmp + + if 'mode' in bond: + bond['mode'] = get_bond_mode(bond['mode']) + + tmp = leaf_node_changed(conf, ['mode']) + if tmp: + bond.update({'shutdown_required': ''}) + + # determine which members have been removed + tmp = leaf_node_changed(conf, ['member', 'interface']) + if tmp: + bond.update({'shutdown_required': ''}) + if 'member' in bond: + bond['member'].update({'interface_remove': tmp }) + else: + bond.update({'member': {'interface_remove': tmp }}) + + if 'member' in bond and 'interface' in bond['member']: + for interface, interface_config in bond['member']['interface'].items(): + # Check if we are a member of another bond device + tmp = is_member(conf, interface, 'bridge') + if tmp: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp and tmp != bond['ifname']: + interface_config.update({'is_bond_member' : tmp}) - # initialize kernel module if not loaded - if not os.path.isfile('/sys/class/net/bonding_masters'): - import syslog - syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if call('modprobe bonding max_bonds=0 miimon=250') != 0: - syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") - raise ConfigError("failed loading bonding kernel module") - - # check if bond has been removed - cfg_base = 'interfaces bonding ' + ifname - if not conf.exists(cfg_base): - bond = deepcopy(default_config_data) - bond['intf'] = ifname - bond['deleted'] = True - return bond - - # set new configuration level - conf.set_level(cfg_base) - - bond, disabled = intf_to_dict(conf, default_config_data) - - # ARP link monitoring frequency in milliseconds - if conf.exists('arp-monitor interval'): - bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) - - # IP address to use for ARP monitoring - if conf.exists('arp-monitor target'): - bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') - - # Bonding transmit hash policy - if conf.exists('hash-policy'): - bond['hash_policy'] = conf.return_value('hash-policy') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - bond['ip_proxy_arp_pvlan'] = 1 - - # Bonding mode - if conf.exists('mode'): - act_mode = conf.return_value('mode') - eff_mode = conf.return_effective_value('mode') - if not (act_mode == eff_mode): - bond['shutdown_required'] = True - - bond['mode'] = get_bond_mode(act_mode) - - # determine bond member interfaces (currently configured) - bond['member'] = conf.return_values('member interface') - - # We can not call conf.return_effective_values() as it would not work - # on reboots. Reboots/First boot will return that running config and - # saved config is the same, thus on a reboot the bond members will - # not be added all (https://phabricator.vyos.net/T2030) - live_members = BondIf(bond['intf']).get_slaves() - if not (bond['member'] == live_members): - bond['shutdown_required'] = True - - # Primary device interface - if conf.exists('primary'): - bond['primary'] = conf.return_value('primary') - - add_to_dict(conf, disabled, bond, 'vif', 'vif') - add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s') + # bond members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) return bond def verify(bond): - if bond['deleted']: - if bond['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{bond["intf"]}" as it is a ' - f'member of bridge "{bond["is_bridge_member"]}"!')) - + if 'deleted' in bond: + verify_bridge_delete(bond) return None - if len(bond['arp_mon_tgt']) > 16: - raise ConfigError('The maximum number of arp-monitor targets is 16') + if 'arp_monitor' in bond: + if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: + raise ConfigError('The maximum number of arp-monitor targets is 16') + + if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') - if bond['primary']: + if 'primary' in bond: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError(( - 'Mode dependency failed, primary not supported in mode ' - f'"{bond["mode"]}"!')) - - if ( bond['is_bridge_member'] - and ( bond['address'] - or bond['ipv6_eui64_prefix'] - or bond['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{bond["intf"]}" ' - f'as it is a member of bridge "{bond["is_bridge_member"]}"!')) - - if bond['vrf']: - if bond['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{bond["vrf"]}" does not exist') - - if bond['is_bridge_member']: - raise ConfigError(( - f'Interface "{bond["intf"]}" cannot be member of VRF ' - f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} ' - f'at the same time!')) + raise ConfigError('Option primary - mode dependency failed, not' + 'supported in mode {mode}!'.format(**bond)) + + verify_address(bond) + verify_dhcpv6(bond) + verify_vrf(bond) # use common function to verify VLAN configuration verify_vlan_config(bond) - conf = Config() - for intf in bond['member']: - # check if member interface is "real" - if intf not in interfaces(): - raise ConfigError(f'Interface {intf} does not exist!') - - # a bonding member interface is only allowed to be assigned to one bond! - all_bonds = conf.list_nodes('interfaces bonding') - # We do not need to check our own bond - all_bonds.remove(bond['intf']) - for tmp in all_bonds: - if conf.exists('interfaces bonding {tmp} member interface {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of bond "{tmp}"!')) - - # can not add interfaces with an assigned address to a bond - if has_address_configured(conf, intf): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it has an address assigned!')) - - # bond members are not allowed to be bridge members - tmp = is_member(conf, intf, 'bridge') - if tmp: - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of bridge "{tmp}"!')) - - # bond members are not allowed to be vrrp members - for tmp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group {tmp} interface {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of VRRP group "{tmp}"!')) - - # bond members are not allowed to be underlaying psuedo-ethernet devices - for tmp in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already the link of pseudo-ethernet "{tmp}"!')) - - # bond members are not allowed to be underlaying vxlan devices - for tmp in conf.list_nodes('interfaces vxlan'): - if conf.exists('interfaces vxlan {tmp} link {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already the link of VXLAN "{tmp}"!')) - - if bond['primary']: - if bond['primary'] not in bond['member']: - raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member') + bond_name = bond['ifname'] + if 'member' in bond: + member = bond.get('member') + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", ' + + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bond') + + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') + + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') + + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') + + + if 'primary' in bond: + if bond['primary'] not in bond['member']['interface']: + raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface') if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('primary interface only works for mode active-backup, ' \ 'transmit-load-balance or adaptive-load-balance') - if bond['arp_mon_intvl'] > 0: - if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: - raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ - 'transmit-load-balance or adaptive-load-balance') - return None def generate(bond): return None def apply(bond): - b = BondIf(bond['intf']) + b = BondIf(bond['ifname']) - if bond['deleted']: + if 'deleted' in bond: # delete interface b.remove() else: - # ARP link monitoring frequency, reset miimon when arp-montior is inactive - # this is done inside BondIf automatically - b.set_arp_interval(bond['arp_mon_intvl']) - - # ARP monitor targets need to be synchronized between sysfs and CLI. - # Unfortunately an address can't be send twice to sysfs as this will - # result in the following exception: OSError: [Errno 22] Invalid argument. - # - # We remove ALL adresses prior adding new ones, this will remove addresses - # added manually by the user too - but as we are limited to 16 adresses - # from the kernel side this looks valid to me. We won't run into an error - # when a user added manual adresses which would result in having more - # then 16 adresses in total. - arp_tgt_addr = list(map(str, b.get_arp_ip_target().split())) - for addr in arp_tgt_addr: - b.set_arp_ip_target('-' + addr) - - # Add configured ARP target addresses - for addr in bond['arp_mon_tgt']: - b.set_arp_ip_target('+' + addr) - - # update interface description used e.g. within SNMP - b.set_alias(bond['description']) - - if bond['dhcp_client_id']: - b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] - - if bond['dhcp_hostname']: - b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] - - if bond['dhcp_vendor_class_id']: - b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] - - if bond['dhcpv6_prm_only']: - b.dhcp.v6.options['dhcpv6_prm_only'] = True - - if bond['dhcpv6_temporary']: - b.dhcp.v6.options['dhcpv6_temporary'] = True - - if bond['dhcpv6_pd_length']: - b.dhcp.v6.options['dhcpv6_pd_length'] = bond['dhcpv6_pd_length'] - - if bond['dhcpv6_pd_interfaces']: - b.dhcp.v6.options['dhcpv6_pd_interfaces'] = bond['dhcpv6_pd_interfaces'] - - # ignore link state changes - b.set_link_detect(bond['disable_link_detect']) - # Bonding transmit hash policy - b.set_hash_policy(bond['hash_policy']) - # configure ARP cache timeout in milliseconds - b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) - # configure ARP filter configuration - b.set_arp_filter(bond['ip_disable_arp_filter']) - # configure ARP accept - b.set_arp_accept(bond['ip_enable_arp_accept']) - # configure ARP announce - b.set_arp_announce(bond['ip_enable_arp_announce']) - # configure ARP ignore - b.set_arp_ignore(bond['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - b.set_proxy_arp(bond['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) - # IPv6 accept RA - b.set_ipv6_accept_ra(bond['ipv6_accept_ra']) - # IPv6 address autoconfiguration - b.set_ipv6_autoconf(bond['ipv6_autoconf']) - # IPv6 forwarding - b.set_ipv6_forwarding(bond['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in bond['ipv6_eui64_prefix_remove']: - b.del_ipv6_eui64_address(addr) - - # Change interface MAC address - if bond['mac']: - b.set_mac(bond['mac']) - - # Add IPv6 EUI-based addresses - for addr in bond['ipv6_eui64_prefix']: - b.add_ipv6_eui64_address(addr) - - # Maximum Transmission Unit (MTU) - b.set_mtu(bond['mtu']) - - # Primary device interface - if bond['primary']: - b.set_primary(bond['primary']) - - # Some parameters can not be changed when the bond is up. - if bond['shutdown_required']: - # Disable bond prior changing of certain properties - b.set_admin_state('down') - - # The bonding mode can not be changed when there are interfaces enslaved - # to this bond, thus we will free all interfaces from the bond first! - for intf in b.get_slaves(): - b.del_port(intf) - - # Bonding policy/mode - b.set_mode(bond['mode']) - - # Add (enslave) interfaces to bond - for intf in bond['member']: - # if we've come here we already verified the interface doesn't - # have addresses configured so just flush any remaining ones - cmd(f'ip addr flush dev "{intf}"') - b.add_port(intf) - - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not bond['disable']: - b.set_admin_state('up') - else: - b.set_admin_state('down') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bond['address_remove']: - b.del_addr(addr) - for addr in bond['address']: - b.add_addr(addr) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not bond['is_bridge_member']: - b.set_vrf(bond['vrf']) - - # re-add ourselves to any bridge we might have fallen out of - if bond['is_bridge_member']: - b.add_to_bridge(bond['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(b, bond) + b.update(bond) return None diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 1e4fa5816..ee8e85e73 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -16,251 +16,102 @@ import os -from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BridgeIf, Section -from vyos.ifconfig.stp import STP -from vyos.configdict import list_diff, interface_default_data -from vyos.validate import is_member, has_address_configured from vyos.config import Config -from vyos.util import cmd, get_bridge_member_config +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_vrf +from vyos.ifconfig import BridgeIf +from vyos.validate import is_member, has_address_configured +from vyos.xml import defaults + +from vyos.util import cmd from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'aging': 300, - 'arp_cache_tmo': 30, - 'deleted': False, - 'forwarding_delay': 14, - 'hello_time': 2, - 'igmp_querier': 0, - 'intf': '', - 'max_age': 20, - 'member': [], - 'member_remove': [], - 'priority': 32768, - 'stp': 0 -} - def get_config(): - bridge = deepcopy(default_config_data) + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # Check if bridge has been removed - if not conf.exists('interfaces bridge ' + bridge['intf']): - bridge['deleted'] = True - return bridge - - # set new configuration level - conf.set_level('interfaces bridge ' + bridge['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - bridge['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - bridge['address_remove'] = list_diff(eff_addr, bridge['address']) - - # retrieve aging - how long addresses are retained - if conf.exists('aging'): - bridge['aging'] = int(conf.return_value('aging')) - - # retrieve interface description - if conf.exists('description'): - bridge['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True - - # Disable this bridge interface - if conf.exists('disable'): - bridge['disable'] = True - - # Ignore link state changes - if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = 2 - - # Forwarding delay - if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) - - # Hello packet advertisment interval - if conf.exists('hello-time'): - bridge['hello_time'] = int(conf.return_value('hello-time')) - - # Enable Internet Group Management Protocol (IGMP) querier - if conf.exists('igmp querier'): - bridge['igmp_querier'] = 1 - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - bridge['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - bridge['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - bridge['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - bridge['ip_enable_arp_ignore'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - bridge['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - - # Determine currently effective EUI64 addresses - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('ipv6 address eui64') - bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix']) - - # Remove the default link-local address if set. - if conf.exists('ipv6 address no-default-link-local'): - bridge['ipv6_eui64_prefix_remove'].append('fe80::/64') - else: - # add the link-local by default to make IPv6 work - bridge['ipv6_eui64_prefix'].append('fe80::/64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - bridge['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # Media Access Control (MAC) address - if conf.exists('mac'): - bridge['mac'] = conf.return_value('mac') - - # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses - # before re-adding them - if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge') - and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ): - bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix'] - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']: - bridge['ipv6_accept_ra'] = 2 - - # Interval at which neighbor bridges are removed - if conf.exists('max-age'): - bridge['max_age'] = int(conf.return_value('max-age')) - - # Determine bridge member interface (currently configured) - for intf in conf.list_nodes('member interface'): - # defaults are stored in util.py (they can't be here as all interface - # scripts use the function) - memberconf = get_bridge_member_config(conf, bridge['intf'], intf) - if memberconf: - memberconf['name'] = intf - bridge['member'].append(memberconf) - - # Determine bridge member interface (currently effective) - to determine which - # interfaces is no longer assigend to the bridge and thus can be removed - eff_intf = conf.list_effective_nodes('member interface') - act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = list_diff(eff_intf, act_intf) - - # Priority for this bridge - if conf.exists('priority'): - bridge['priority'] = int(conf.return_value('priority')) - - # Enable spanning tree protocol - if conf.exists('stp'): - bridge['stp'] = 1 - - # retrieve VRF instance - if conf.exists('vrf'): - bridge['vrf'] = conf.return_value('vrf') + base = ['interfaces', 'bridge'] + bridge = get_interface_dict(conf, base) + + # determine which members have been removed + tmp = node_changed(conf, ['member', 'interface']) + if tmp: + if 'member' in bridge: + bridge['member'].update({'interface_remove': tmp }) + else: + bridge.update({'member': {'interface_remove': tmp }}) + + if 'member' in bridge and 'interface' in bridge['member']: + # XXX TT2665 we need a copy of the dict keys for iteration, else we will get: + # RuntimeError: dictionary changed size during iteration + for interface in list(bridge['member']['interface']): + for key in ['cost', 'priority']: + if interface == key: + del bridge['member']['interface'][key] + continue + + # the default dictionary is not properly paged into the dict (see T2665) + # thus we will ammend it ourself + default_member_values = defaults(base + ['member', 'interface']) + for interface, interface_config in bridge['member']['interface'].items(): + interface_config.update(default_member_values) + + # Check if we are a member of another bridge device + tmp = is_member(conf, interface, 'bridge') + if tmp and tmp != bridge['ifname']: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp: + interface_config.update({'is_bond_member' : tmp}) + + # Bridge members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) return bridge def verify(bridge): - if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + if 'deleted' in bridge: + return None - vrf_name = bridge['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + verify_dhcpv6(bridge) + verify_vrf(bridge) - conf = Config() - for intf in bridge['member']: - # the interface must exist prior adding it to a bridge - if intf['name'] not in interfaces(): - raise ConfigError(( - f'Cannot add nonexistent interface "{intf["name"]}" ' - f'to bridge "{bridge["intf"]}"')) + if 'member' in bridge: + member = bridge.get('member') + bridge_name = bridge['ifname'] + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bridge "{bridge_name}", ' - if intf['name'] == 'lo': - raise ConfigError('Loopback interface "lo" can not be added to a bridge') + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bridge') - # bridge members aren't allowed to be members of another bridge - for br in conf.list_nodes('interfaces bridge'): - # it makes no sense to verify ourself in this case - if br == bridge['intf']: - continue + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') - tmp = conf.list_nodes(f'interfaces bridge {br} member interface') - if intf['name'] in tmp: - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it is already a member of bridge "{br}"!')) + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') - # bridge members are not allowed to be bond members - tmp = is_member(conf, intf['name'], 'bonding') - if tmp: - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!')) + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') - # bridge members must not have an assigned address - if has_address_configured(conf, intf['name']): - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it has an address assigned!')) + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') return None @@ -268,120 +119,12 @@ def generate(bridge): return None def apply(bridge): - br = BridgeIf(bridge['intf']) - - if bridge['deleted']: + br = BridgeIf(bridge['ifname']) + if 'deleted' in bridge: # delete interface br.remove() else: - # enable interface - br.set_admin_state('up') - # set ageing time - br.set_ageing_time(bridge['aging']) - # set bridge forward delay - br.set_forward_delay(bridge['forwarding_delay']) - # set hello time - br.set_hello_time(bridge['hello_time']) - # configure ARP filter configuration - br.set_arp_filter(bridge['ip_disable_arp_filter']) - # configure ARP accept - br.set_arp_accept(bridge['ip_enable_arp_accept']) - # configure ARP announce - br.set_arp_announce(bridge['ip_enable_arp_announce']) - # configure ARP ignore - br.set_arp_ignore(bridge['ip_enable_arp_ignore']) - # IPv6 accept RA - br.set_ipv6_accept_ra(bridge['ipv6_accept_ra']) - # IPv6 address autoconfiguration - br.set_ipv6_autoconf(bridge['ipv6_autoconf']) - # IPv6 forwarding - br.set_ipv6_forwarding(bridge['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect']) - # set max message age - br.set_max_age(bridge['max_age']) - # set bridge priority - br.set_priority(bridge['priority']) - # turn stp on/off - br.set_stp(bridge['stp']) - # enable or disable IGMP querier - br.set_multicast_querier(bridge['igmp_querier']) - # update interface description used e.g. within SNMP - br.set_alias(bridge['description']) - - if bridge['dhcp_client_id']: - br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id'] - - if bridge['dhcp_hostname']: - br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname'] - - if bridge['dhcp_vendor_class_id']: - br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - - if bridge['dhcpv6_prm_only']: - br.dhcp.v6.options['dhcpv6_prm_only'] = True - - if bridge['dhcpv6_temporary']: - br.dhcp.v6.options['dhcpv6_temporary'] = True - - if bridge['dhcpv6_pd_length']: - br.dhcp.v6.options['dhcpv6_pd_length'] = br['dhcpv6_pd_length'] - - if bridge['dhcpv6_pd_interfaces']: - br.dhcp.v6.options['dhcpv6_pd_interfaces'] = br['dhcpv6_pd_interfaces'] - - # assign/remove VRF - br.set_vrf(bridge['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - # (adding members to a fresh bridge changes its MAC too) - for addr in bridge['ipv6_eui64_prefix_remove']: - br.del_ipv6_eui64_address(addr) - - # remove interface from bridge - for intf in bridge['member_remove']: - br.del_port(intf) - - # add interfaces to bridge - for member in bridge['member']: - # if we've come here we already verified the interface doesn't - # have addresses configured so just flush any remaining ones - cmd(f'ip addr flush dev "{member["name"]}"') - br.add_port(member['name']) - - # Change interface MAC address - if bridge['mac']: - br.set_mac(bridge['mac']) - - # Add IPv6 EUI-based addresses (must be done after adding the - # 1st bridge member or setting its MAC) - for addr in bridge['ipv6_eui64_prefix']: - br.add_ipv6_eui64_address(addr) - - # up/down interface - if bridge['disable']: - br.set_admin_state('down') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bridge['address_remove']: - br.del_addr(addr) - for addr in bridge['address']: - br.add_addr(addr) - - STPBridgeIf = STP.enable(BridgeIf) - # configure additional bridge member options - for member in bridge['member']: - i = STPBridgeIf(member['name']) - # configure ARP cache timeout - i.set_arp_cache_tmo(member['arp_cache_tmo']) - # ignore link state changes - i.set_link_detect(member['disable_link_detect']) - # set bridge port path cost - i.set_path_cost(member['cost']) - # set bridge port path priority - i.set_path_priority(member['priority']) + br.update(bridge) return None diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 2d62420a6..8df86c8ea 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -19,41 +19,23 @@ import os from sys import exit from vyos.config import Config +from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.ifconfig import DummyIf -from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = ['interfaces', 'dummy', ifname] - - dummy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if dummy == {}: - dummy.update({'deleted' : ''}) - - # store interface instance name in dictionary - dummy.update({'ifname': ifname}) - - # check if we are a member of any bridge - bridge = is_member(conf, ifname, 'bridge') - if bridge: - tmp = {'is_bridge_member' : bridge} - dummy.update(tmp) - + base = ['interfaces', 'dummy'] + dummy = get_interface_dict(conf, base) return dummy def verify(dummy): diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 8b895c4d2..10758e35a 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -17,295 +17,65 @@ import os from sys import exit -from copy import deepcopy -from netifaces import interfaces -from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.validate import is_member from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_address +from vyos.configverify import verify_vrf +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import EthernetIf from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'deleted': False, - 'duplex': 'auto', - 'flow_control': 'on', - 'hw_id': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'is_bond_member': False, - 'intf': '', - 'offload_gro': 'off', - 'offload_gso': 'off', - 'offload_sg': 'off', - 'offload_tso': 'off', - 'offload_ufo': 'off', - 'speed': 'auto', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], - 'vrf': '' -} - - def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() + base = ['interfaces', 'ethernet'] + ethernet = get_interface_dict(conf, base) + return ethernet - # check if ethernet interface has been removed - cfg_base = ['interfaces', 'ethernet', ifname] - if not conf.exists(cfg_base): - eth = deepcopy(default_config_data) - eth['intf'] = ifname - eth['deleted'] = True - # we can not bail out early as ethernet interface can not be removed - # Kernel will complain with: RTNETLINK answers: Operation not supported. - # Thus we need to remove individual settings - return eth - - # set new configuration level - conf.set_level(cfg_base) - - eth, disabled = intf_to_dict(conf, default_config_data) - - # disable ethernet flow control (pause frames) - if conf.exists('disable-flow-control'): - eth['flow_control'] = 'off' - - # retrieve real hardware address - if conf.exists('hw-id'): - eth['hw_id'] = conf.return_value('hw-id') - - # interface duplex - if conf.exists('duplex'): - eth['duplex'] = conf.return_value('duplex') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - eth['ip_proxy_arp_pvlan'] = 1 - - # check if we are a member of any bond - eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding') - - # GRO (generic receive offload) - if conf.exists('offload-options generic-receive'): - eth['offload_gro'] = conf.return_value('offload-options generic-receive') - - # GSO (generic segmentation offload) - if conf.exists('offload-options generic-segmentation'): - eth['offload_gso'] = conf.return_value('offload-options generic-segmentation') - - # scatter-gather option - if conf.exists('offload-options scatter-gather'): - eth['offload_sg'] = conf.return_value('offload-options scatter-gather') - - # TSO (TCP segmentation offloading) - if conf.exists('offload-options tcp-segmentation'): - eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation') - - # UDP fragmentation offloading - if conf.exists('offload-options udp-fragmentation'): - eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation') - - # interface speed - if conf.exists('speed'): - eth['speed'] = conf.return_value('speed') - - # remove default IPv6 link-local address if member of a bond - if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']: - eth['ipv6_eui64_prefix'].remove('fe80::/64') - eth['ipv6_eui64_prefix_remove'].append('fe80::/64') - - add_to_dict(conf, disabled, eth, 'vif', 'vif') - add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s') - - return eth - - -def verify(eth): - if eth['deleted']: +def verify(ethernet): + if 'deleted' in ethernet: return None - if eth['intf'] not in interfaces(): - raise ConfigError(f"Interface ethernet {eth['intf']} does not exist") + verify_interface_exists(ethernet) - if eth['speed'] == 'auto': - if eth['duplex'] != 'auto': + if ethernet.get('speed', None) == 'auto': + if ethernet.get('duplex', None) != 'auto': raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') - if eth['duplex'] == 'auto': - if eth['speed'] != 'auto': + if ethernet.get('duplex', None) == 'auto': + if ethernet.get('speed', None) != 'auto': raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') - if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member'] - - if ( memberof - and ( eth['address'] - or eth['ipv6_eui64_prefix'] - or eth['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{eth["intf"]}" ' - f'as it is a member of "{memberof}"!')) + verify_dhcpv6(ethernet) + verify_address(ethernet) + verify_vrf(ethernet) - if eth['vrf']: - if eth['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{eth["vrf"]}" does not exist') - - if memberof: - raise ConfigError(( - f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" ' - f'and "{memberof}" at the same time!')) - - if eth['mac'] and eth['is_bond_member']: - print('WARNING: "mac {0}" command will be ignored because {1} is a part of {2}'\ - .format(eth['mac'], eth['intf'], eth['is_bond_member'])) + if {'is_bond_member', 'mac'} <= set(ethernet): + print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' + f'is a member of bond "{is_bond_member}"'.format(**ethernet)) # use common function to verify VLAN configuration - verify_vlan_config(eth) + verify_vlan_config(ethernet) return None -def generate(eth): +def generate(ethernet): return None -def apply(eth): - e = EthernetIf(eth['intf']) - if eth['deleted']: - # apply all vlans to interface (they need removing too) - apply_all_vlans(e, eth) - +def apply(ethernet): + e = EthernetIf(ethernet['ifname']) + if 'deleted' in ethernet: # delete interface e.remove() else: - # update interface description used e.g. within SNMP - e.set_alias(eth['description']) - - if eth['dhcp_client_id']: - e.dhcp.v4.options['client_id'] = eth['dhcp_client_id'] - - if eth['dhcp_hostname']: - e.dhcp.v4.options['hostname'] = eth['dhcp_hostname'] - - if eth['dhcp_vendor_class_id']: - e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id'] - - if eth['dhcpv6_prm_only']: - e.dhcp.v6.options['dhcpv6_prm_only'] = True - - if eth['dhcpv6_temporary']: - e.dhcp.v6.options['dhcpv6_temporary'] = True - - if eth['dhcpv6_pd_length']: - e.dhcp.v6.options['dhcpv6_pd_length'] = eth['dhcpv6_pd_length'] - - if eth['dhcpv6_pd_interfaces']: - e.dhcp.v6.options['dhcpv6_pd_interfaces'] = eth['dhcpv6_pd_interfaces'] - - # ignore link state changes - e.set_link_detect(eth['disable_link_detect']) - # disable ethernet flow control (pause frames) - e.set_flow_control(eth['flow_control']) - # configure ARP cache timeout in milliseconds - e.set_arp_cache_tmo(eth['ip_arp_cache_tmo']) - # configure ARP filter configuration - e.set_arp_filter(eth['ip_disable_arp_filter']) - # configure ARP accept - e.set_arp_accept(eth['ip_enable_arp_accept']) - # configure ARP announce - e.set_arp_announce(eth['ip_enable_arp_announce']) - # configure ARP ignore - e.set_arp_ignore(eth['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - e.set_proxy_arp(eth['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan']) - # IPv6 accept RA - e.set_ipv6_accept_ra(eth['ipv6_accept_ra']) - # IPv6 address autoconfiguration - e.set_ipv6_autoconf(eth['ipv6_autoconf']) - # IPv6 forwarding - e.set_ipv6_forwarding(eth['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in eth['ipv6_eui64_prefix_remove']: - e.del_ipv6_eui64_address(addr) - - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed. Skip if bond member. - if not eth['is_bond_member']: - if eth['mac']: - e.set_mac(eth['mac']) - elif eth['hw_id']: - e.set_mac(eth['hw_id']) - - # Add IPv6 EUI-based addresses - for addr in eth['ipv6_eui64_prefix']: - e.add_ipv6_eui64_address(addr) - - # Maximum Transmission Unit (MTU) - e.set_mtu(eth['mtu']) - - # GRO (generic receive offload) - e.set_gro(eth['offload_gro']) - - # GSO (generic segmentation offload) - e.set_gso(eth['offload_gso']) - - # scatter-gather option - e.set_sg(eth['offload_sg']) - - # TSO (TCP segmentation offloading) - e.set_tso(eth['offload_tso']) - - # UDP fragmentation offloading - e.set_ufo(eth['offload_ufo']) - - # Set physical interface speed and duplex - e.set_speed_duplex(eth['speed'], eth['duplex']) - - # Enable/Disable interface - if eth['disable']: - e.set_admin_state('down') - else: - e.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in eth['address_remove']: - e.del_addr(addr) - for addr in eth['address']: - e.add_addr(addr) - - # assign/remove VRF (ONLY when not a member of a bridge or bond, - # otherwise 'nomaster' removes it from it) - if not ( eth['is_bridge_member'] or eth['is_bond_member'] ): - e.set_vrf(eth['vrf']) - - # re-add ourselves to any bridge we might have fallen out of - if eth['is_bridge_member']: - e.add_to_bridge(eth['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(e, eth) + e.update(ethernet) if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 31f6eb6b5..1104bd3c0 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -21,102 +21,37 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete from vyos.ifconfig import GeneveIf -from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp': 0, - 'is_bridge_member': False, - 'mtu': 1500, - 'remote': '', - 'vni': '' -} - def get_config(): - geneve = deepcopy(default_config_data) + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces geneve ' + geneve['intf']): - geneve['deleted'] = True - return geneve - - # set new configuration level - conf.set_level('interfaces geneve ' + geneve['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - geneve['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - geneve['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - geneve['disable'] = True - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - geneve['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - geneve['ip_proxy_arp'] = 1 - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - geneve['mtu'] = int(conf.return_value('mtu')) - - # Remote address of GENEVE tunnel - if conf.exists('remote'): - geneve['remote'] = conf.return_value('remote') - - # Virtual Network Identifier - if conf.exists('vni'): - geneve['vni'] = conf.return_value('vni') - + base = ['interfaces', 'geneve'] + geneve = get_interface_dict(conf, base) return geneve - def verify(geneve): - if geneve['deleted']: - if geneve['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{geneve["intf"]}" as it is a ' - f'member of bridge "{geneve["is_bridge_member"]}"!')) - + if 'deleted' in geneve: + verify_bridge_delete(geneve) return None - if geneve['is_bridge_member'] and geneve['address']: - raise ConfigError(( - f'Cannot assign address to interface "{geneve["intf"]}" ' - f'as it is a member of bridge "{geneve["is_bridge_member"]}"!')) + verify_address(geneve) - if not geneve['remote']: - raise ConfigError('GENEVE remote must be configured') + if 'remote' not in geneve: + raise ConfigError('Remote side must be configured') - if not geneve['vni']: - raise ConfigError('GENEVE VNI must be configured') + if 'vni' not in geneve: + raise ConfigError('VNI must be configured') return None @@ -127,13 +62,13 @@ def generate(geneve): def apply(geneve): # Check if GENEVE interface already exists - if geneve['intf'] in interfaces(): - g = GeneveIf(geneve['intf']) + if geneve['ifname'] in interfaces(): + g = GeneveIf(geneve['ifname']) # GENEVE is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. g.remove() - if not geneve['deleted']: + if 'deleted' not in geneve: # GENEVE interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig @@ -144,32 +79,8 @@ def apply(geneve): conf['remote'] = geneve['remote'] # Finally create the new interface - g = GeneveIf(geneve['intf'], **conf) - # update interface description used e.g. by SNMP - g.set_alias(geneve['description']) - # Maximum Transfer Unit (MTU) - g.set_mtu(geneve['mtu']) - - # configure ARP cache timeout in milliseconds - g.set_arp_cache_tmo(geneve['ip_arp_cache_tmo']) - # Enable proxy-arp on this interface - g.set_proxy_arp(geneve['ip_proxy_arp']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in geneve['address']: - g.add_addr(addr) - - # As the GENEVE interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not geneve['disable']: - g.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if geneve['is_bridge_member']: - g.add_to_bridge(geneve['is_bridge_member']) + g = GeneveIf(geneve['ifname'], **conf) + g.update(geneve) return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 4ff0bcb57..0978df5b6 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -21,200 +21,65 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.ifconfig import L2TPv3If, Interface +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import L2TPv3If +from vyos.util import check_kmod +from vyos.validate import is_addr_assigned from vyos import ConfigError -from vyos.util import call -from vyos.validate import is_member, is_addr_assigned - from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'encapsulation': 'udp', - 'local_address': '', - 'local_port': 5000, - 'intf': '', - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'mtu': 1488, - 'peer_session_id': '', - 'peer_tunnel_id': '', - 'remote_address': '', - 'remote_port': 5000, - 'session_id': '', - 'tunnel_id': '' -} +k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] -def check_kmod(): - modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') def get_config(): - l2tpv3 = deepcopy(default_config_data) + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() + base = ['interfaces', 'l2tpv3'] + l2tpv3 = get_interface_dict(conf, base) - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member of a bridge - l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']): - l2tpv3['deleted'] = True - interface = l2tpv3['intf'] - - # to delete the l2tpv3 interface we need the current tunnel_id and session_id - if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'): - l2tpv3['tunnel_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} tunnel-id') - - if conf.exists_effective(f'interfaces l2tpv3 {interface} session-id'): - l2tpv3['session_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} session-id') - - return l2tpv3 - - # set new configuration level - conf.set_level('interfaces l2tpv3 ' + l2tpv3['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - l2tpv3['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - l2tpv3['description'] = conf.return_value('description') - - # get tunnel destination port - if conf.exists('destination-port'): - l2tpv3['remote_port'] = int(conf.return_value('destination-port')) - - # Disable this interface - if conf.exists('disable'): - l2tpv3['disable'] = True - - # get tunnel encapsulation type - if conf.exists('encapsulation'): - l2tpv3['encapsulation'] = conf.return_value('encapsulation') - - # get tunnel local ip address - if conf.exists('local-ip'): - l2tpv3['local_address'] = conf.return_value('local-ip') - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - l2tpv3['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - - # Remove the default link-local address if set. - if not ( conf.exists('ipv6 address no-default-link-local') or - l2tpv3['is_bridge_member'] ): - # add the link-local by default to make IPv6 work - l2tpv3['ipv6_eui64_prefix'].append('fe80::/64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - l2tpv3['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # L2TPv3 is "special" the default MTU is 1488 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + l2tpv3['mtu'] = '1488' - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']: - l2tpv3['ipv6_accept_ra'] = 2 + # To delete an l2tpv3 interface we need the current tunnel and session-id + if 'deleted' in l2tpv3: + tmp = leaf_node_changed(conf, ['tunnel-id']) + l2tpv3.update({'tunnel_id': tmp}) - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - l2tpv3['mtu'] = int(conf.return_value('mtu')) - - # Remote session id - if conf.exists('peer-session-id'): - l2tpv3['peer_session_id'] = conf.return_value('peer-session-id') - - # Remote tunnel id - if conf.exists('peer-tunnel-id'): - l2tpv3['peer_tunnel_id'] = conf.return_value('peer-tunnel-id') - - # Remote address of L2TPv3 tunnel - if conf.exists('remote-ip'): - l2tpv3['remote_address'] = conf.return_value('remote-ip') - - # Local session id - if conf.exists('session-id'): - l2tpv3['session_id'] = conf.return_value('session-id') - - # get local tunnel port - if conf.exists('source-port'): - l2tpv3['local_port'] = conf.return_value('source-port') - - # get local tunnel id - if conf.exists('tunnel-id'): - l2tpv3['tunnel_id'] = conf.return_value('tunnel-id') + tmp = leaf_node_changed(conf, ['session-id']) + l2tpv3.update({'session_id': tmp}) return l2tpv3 - def verify(l2tpv3): - interface = l2tpv3['intf'] - - if l2tpv3['deleted']: - if l2tpv3['is_bridge_member']: - raise ConfigError(( - f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a ' - f'member of bridge "{l2tpv3["is_bridge_member"]}"!')) - + if 'deleted' in l2tpv3: + verify_bridge_delete(l2tpv3) return None - if not l2tpv3['local_address']: - raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}') - - if not is_addr_assigned(l2tpv3['local_address']): - raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}') + interface = l2tpv3['ifname'] - if not l2tpv3['remote_address']: - raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}') + for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id', + 'session_id', 'peer_session_id']: + if key not in l2tpv3: + tmp = key.replace('_', '-') + raise ConfigError(f'L2TPv3 {tmp} must be configured!') - if not l2tpv3['tunnel_id']: - raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}') - - if not l2tpv3['peer_tunnel_id']: - raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}') - - if not l2tpv3['session_id']: - raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}') - - if not l2tpv3['peer_session_id']: - raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}') - - if ( l2tpv3['is_bridge_member'] - and ( l2tpv3['address'] - or l2tpv3['ipv6_eui64_prefix'] - or l2tpv3['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{l2tpv3["intf"]}" ' - f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!')) + if not is_addr_assigned(l2tpv3['local_ip']): + raise ConfigError('L2TPv3 local-ip address ' + '"{local_ip}" is not configured!'.format(**l2tpv3)) + verify_address(l2tpv3) return None - def generate(l2tpv3): return None @@ -225,65 +90,34 @@ def apply(l2tpv3): conf = deepcopy(L2TPv3If.get_config()) # Check if L2TPv3 interface already exists - if l2tpv3['intf'] in interfaces(): + if l2tpv3['ifname'] in interfaces(): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply # always delete it first. conf['session_id'] = l2tpv3['session_id'] conf['tunnel_id'] = l2tpv3['tunnel_id'] - l = L2TPv3If(l2tpv3['intf'], **conf) + l = L2TPv3If(l2tpv3['ifname'], **conf) l.remove() - if not l2tpv3['deleted']: + if 'deleted' not in l2tpv3: conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id'] - conf['local_port'] = l2tpv3['local_port'] - conf['remote_port'] = l2tpv3['remote_port'] + conf['local_port'] = l2tpv3['source_port'] + conf['remote_port'] = l2tpv3['destination_port'] conf['encapsulation'] = l2tpv3['encapsulation'] - conf['local_address'] = l2tpv3['local_address'] - conf['remote_address'] = l2tpv3['remote_address'] + conf['local_address'] = l2tpv3['local_ip'] + conf['remote_address'] = l2tpv3['remote_ip'] conf['session_id'] = l2tpv3['session_id'] conf['tunnel_id'] = l2tpv3['tunnel_id'] conf['peer_session_id'] = l2tpv3['peer_session_id'] # Finally create the new interface - l = L2TPv3If(l2tpv3['intf'], **conf) - # update interface description used e.g. by SNMP - l.set_alias(l2tpv3['description']) - # Maximum Transfer Unit (MTU) - l.set_mtu(l2tpv3['mtu']) - # IPv6 accept RA - l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra']) - # IPv6 address autoconfiguration - l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf']) - # IPv6 forwarding - l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in l2tpv3['address']: - l.add_addr(addr) - - # IPv6 EUI-based addresses - for addr in l2tpv3['ipv6_eui64_prefix']: - l.add_ipv6_eui64_address(addr) - - # As the interface is always disabled first when changing parameters - # we will only re-enable the interface if it is not administratively - # disabled - if not l2tpv3['disable']: - l.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if l2tpv3['is_bridge_member']: - l.add_to_bridge(l2tpv3['is_bridge_member']) + l = L2TPv3If(l2tpv3['ifname'], **conf) + l.update(l2tpv3) return None if __name__ == '__main__': try: - check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 2368f88a9..0398cd591 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -18,31 +18,21 @@ import os from sys import exit -from vyos.ifconfig import LoopbackIf from vyos.config import Config -from vyos import ConfigError, airbag +from vyos.configdict import get_interface_dict +from vyos.ifconfig import LoopbackIf +from vyos import ConfigError +from vyos import airbag airbag.enable() def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = ['interfaces', 'loopback', ifname] - - loopback = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if loopback == {}: - loopback.update({'deleted' : ''}) - - # store interface instance name in dictionary - loopback.update({'ifname': ifname}) - + base = ['interfaces', 'loopback'] + loopback = get_interface_dict(conf, base) return loopback def verify(loopback): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 56273f71a..ca15212d4 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -20,16 +20,14 @@ from copy import deepcopy from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.ifconfig import MACsecIf from vyos.template import render from vyos.util import call -from vyos.validate import is_member from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_source_interface -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -38,50 +36,25 @@ airbag.enable() wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values base = ['interfaces', 'macsec'] - default_values = defaults(base) + macsec = get_interface_dict(conf, base) - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] - - macsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # Check if interface has been removed - if macsec == {}: - tmp = { - 'deleted' : '', - 'source_interface' : conf.return_effective_value( + if 'deleted' in macsec: + source_interface = conf.return_effective_value( base + ['source-interface']) - } - macsec.update(tmp) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - macsec = dict_merge(default_values, macsec) - - # Add interface instance name into dictionary - macsec.update({'ifname': ifname}) - - # Check if we are a member of any bridge - bridge = is_member(conf, ifname, 'bridge') - if bridge: - tmp = {'is_bridge_member' : bridge} - macsec.update(tmp) + macsec.update({'source_interface': source_interface}) return macsec def verify(macsec): - if 'deleted' in macsec.keys(): + if 'deleted' in macsec: verify_bridge_delete(macsec) return None @@ -89,18 +62,18 @@ def verify(macsec): verify_vrf(macsec) verify_address(macsec) - if not (('security' in macsec.keys()) and - ('cipher' in macsec['security'].keys())): + if not (('security' in macsec) and + ('cipher' in macsec['security'])): raise ConfigError( 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) - if (('security' in macsec.keys()) and - ('encrypt' in macsec['security'].keys())): + if (('security' in macsec) and + ('encrypt' in macsec['security'])): tmp = macsec.get('security') - if not (('mka' in tmp.keys()) and - ('cak' in tmp['mka'].keys()) and - ('ckn' in tmp['mka'].keys())): + if not (('mka' in tmp) and + ('cak' in tmp['mka']) and + ('ckn' in tmp['mka'])): raise ConfigError('Missing mandatory MACsec security ' 'keys as encryption is enabled!') diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 3ee57e83c..928113b49 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -22,51 +22,34 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.configverify import verify_source_interface from vyos.configverify import verify_vrf from vyos.template import render from vyos.util import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values base = ['interfaces', 'pppoe'] - default_values = defaults(base) - # PPPoE is "special" the default MTU is 1492 - update accordingly - default_values['mtu'] = '1492' - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] + pppoe = get_interface_dict(conf, base) - pppoe = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if pppoe == {}: - pppoe.update({'deleted' : ''}) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - pppoe = dict_merge(default_values, pppoe) - - # Add interface instance name into dictionary - pppoe.update({'ifname': ifname}) + # PPPoE is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + pppoe['mtu'] = '1492' return pppoe def verify(pppoe): - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # bail out early return None @@ -92,7 +75,7 @@ def generate(pppoe): config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # stop DHCPv6-PD client call(f'systemctl stop dhcp6c@{ifname}.service') # Hang-up PPPoE connection @@ -125,16 +108,16 @@ def generate(pppoe): if tmp and len(tmp) > 0: # ipv6.tmpl relies on ifname - this should be made consitent in the # future better then double key-ing the same value - render(config_wide_dhcp6c, 'dhcp-client/ipv6_new.tmpl', pppoe, trim_blocks=True) + render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True) return None def apply(pppoe): - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # bail out early return None - if 'disable' not in pppoe.keys(): + if 'disable' not in pppoe: # Dial PPPoE connection call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 70710e97c..4afea2b3a 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -18,112 +18,44 @@ import os from copy import deepcopy from sys import exit -from netifaces import interfaces from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import MACVLANIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import MACVLANIf from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'deleted': False, - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'source_interface': '', - 'source_interface_changed': False, - 'mode': 'private', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], - 'vrf': '' -} - def get_config(): - peth = deepcopy(default_config_data) + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() + base = ['interfaces', 'pseudo-ethernet'] + peth = get_interface_dict(conf, base) - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - peth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # Check if interface has been removed - cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']] - if not conf.exists(cfg_base): - peth['deleted'] = True - return peth - - # set new configuration level - conf.set_level(cfg_base) - - peth, disabled = intf_to_dict(conf, default_config_data) - - # ARP cache entry timeout in seconds - if conf.exists(['ip', 'arp-cache-timeout']): - peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout'])) - - # Enable private VLAN proxy ARP on this interface - if conf.exists(['ip', 'proxy-arp-pvlan']): - peth['ip_proxy_arp_pvlan'] = 1 - - # Physical interface - if conf.exists(['source-interface']): - peth['source_interface'] = conf.return_value(['source-interface']) - tmp = conf.return_effective_value(['source-interface']) - if tmp != peth['source_interface']: - peth['source_interface_changed'] = True - - # MACvlan mode - if conf.exists(['mode']): - peth['mode'] = conf.return_value(['mode']) - - add_to_dict(conf, disabled, peth, 'vif', 'vif') - add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s') + mode = leaf_node_changed(conf, ['mode']) + if mode: + peth.update({'mode_old' : mode}) return peth def verify(peth): - if peth['deleted']: - if peth['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{peth["intf"]}" as it is a ' - f'member of bridge "{peth["is_bridge_member"]}"!')) - + if 'deleted' in peth: + verify_bridge_delete(peth) return None - if not peth['source_interface']: - raise ConfigError(( - f'Link device must be set for pseudo-ethernet "{peth["intf"]}"')) - - if not peth['source_interface'] in interfaces(): - raise ConfigError(( - f'Pseudo-ethernet "{peth["intf"]}" link device does not exist')) - - if ( peth['is_bridge_member'] - and ( peth['address'] - or peth['ipv6_eui64_prefix'] - or peth['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{peth["intf"]}" ' - f'as it is a member of bridge "{peth["is_bridge_member"]}"!')) - - if peth['vrf']: - if peth['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{peth["vrf"]}" does not exist') - - if peth['is_bridge_member']: - raise ConfigError(( - f'Interface "{peth["intf"]}" cannot be member of VRF ' - f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} ' - f'at the same time!')) + verify_source_interface(peth) + verify_vrf(peth) + verify_address(peth) # use common function to verify VLAN configuration verify_vlan_config(peth) @@ -133,17 +65,16 @@ def generate(peth): return None def apply(peth): - if peth['deleted']: + if 'deleted' in peth: # delete interface - MACVLANIf(peth['intf']).remove() + MACVLANIf(peth['ifname']).remove() return None # Check if MACVLAN interface already exists. Parameters like the underlaying - # source-interface device can not be changed on the fly and the interface + # source-interface device or mode can not be changed on the fly and the interface # needs to be recreated from the bottom. - if peth['intf'] in interfaces(): - if peth['source_interface_changed']: - MACVLANIf(peth['intf']).remove() + if 'mode_old' in peth: + MACVLANIf(peth['ifname']).remove() # MACVLAN interface needs to be created on-block instead of passing a ton # of arguments, I just use a dict that is managed by vyos.ifconfig @@ -155,98 +86,8 @@ def apply(peth): # It is safe to "re-create" the interface always, there is a sanity check # that the interface will only be create if its non existent - p = MACVLANIf(peth['intf'], **conf) - - # update interface description used e.g. within SNMP - p.set_alias(peth['description']) - - if peth['dhcp_client_id']: - p.dhcp.v4.options['client_id'] = peth['dhcp_client_id'] - - if peth['dhcp_hostname']: - p.dhcp.v4.options['hostname'] = peth['dhcp_hostname'] - - if peth['dhcp_vendor_class_id']: - p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id'] - - if peth['dhcpv6_prm_only']: - p.dhcp.v6.options['dhcpv6_prm_only'] = True - - if peth['dhcpv6_temporary']: - p.dhcp.v6.options['dhcpv6_temporary'] = True - - if peth['dhcpv6_pd_length']: - p.dhcp.v6.options['dhcpv6_pd_length'] = peth['dhcpv6_pd_length'] - - if peth['dhcpv6_pd_interfaces']: - p.dhcp.v6.options['dhcpv6_pd_interfaces'] = peth['dhcpv6_pd_interfaces'] - - # ignore link state changes - p.set_link_detect(peth['disable_link_detect']) - # configure ARP cache timeout in milliseconds - p.set_arp_cache_tmo(peth['ip_arp_cache_tmo']) - # configure ARP filter configuration - p.set_arp_filter(peth['ip_disable_arp_filter']) - # configure ARP accept - p.set_arp_accept(peth['ip_enable_arp_accept']) - # configure ARP announce - p.set_arp_announce(peth['ip_enable_arp_announce']) - # configure ARP ignore - p.set_arp_ignore(peth['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - p.set_proxy_arp(peth['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan']) - # IPv6 accept RA - p.set_ipv6_accept_ra(peth['ipv6_accept_ra']) - # IPv6 address autoconfiguration - p.set_ipv6_autoconf(peth['ipv6_autoconf']) - # IPv6 forwarding - p.set_ipv6_forwarding(peth['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not peth['is_bridge_member']: - p.set_vrf(peth['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in peth['ipv6_eui64_prefix_remove']: - p.del_ipv6_eui64_address(addr) - - # Change interface MAC address - if peth['mac']: - p.set_mac(peth['mac']) - - # Add IPv6 EUI-based addresses - for addr in peth['ipv6_eui64_prefix']: - p.add_ipv6_eui64_address(addr) - - # Change interface mode - p.set_mode(peth['mode']) - - # Enable/Disable interface - if peth['disable']: - p.set_admin_state('down') - else: - p.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in peth['address_remove']: - p.del_addr(addr) - for addr in peth['address']: - p.add_addr(addr) - - # re-add ourselves to any bridge we might have fallen out of - if peth['is_bridge_member']: - p.add_to_bridge(peth['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(p, peth) - + p = MACVLANIf(peth['ifname'], **conf) + p.update(peth) return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 39db814b4..47c0bdcb8 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -21,197 +21,61 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface from vyos.ifconfig import VXLANIf, Interface -from vyos.validate import is_member from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'group': '', - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'source_address': '', - 'source_interface': '', - 'mtu': 1450, - 'remote': '', - 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - 'vni': '' -} - def get_config(): - vxlan = deepcopy(default_config_data) + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() + base = ['interfaces', 'vxlan'] + vxlan = get_interface_dict(conf, base) - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces vxlan ' + vxlan['intf']): - vxlan['deleted'] = True - return vxlan - - # set new configuration level - conf.set_level('interfaces vxlan ' + vxlan['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - vxlan['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - vxlan['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - vxlan['disable'] = True - - # VXLAN multicast grou - if conf.exists('group'): - vxlan['group'] = conf.return_value('group') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - vxlan['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - vxlan['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - vxlan['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - vxlan['ip_enable_arp_ignore'] = 1 - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - vxlan['ip_proxy_arp'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - vxlan['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - - # Remove the default link-local address if set. - if not ( conf.exists('ipv6 address no-default-link-local') - or vxlan['is_bridge_member'] ): - # add the link-local by default to make IPv6 work - vxlan['ipv6_eui64_prefix'].append('fe80::/64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - vxlan['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']: - vxlan['ipv6_accept_ra'] = 2 - - # VXLAN source address - if conf.exists('source-address'): - vxlan['source_address'] = conf.return_value('source-address') - - # VXLAN underlay interface - if conf.exists('source-interface'): - vxlan['source_interface'] = conf.return_value('source-interface') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vxlan['mtu'] = int(conf.return_value('mtu')) - - # Remote address of VXLAN tunnel - if conf.exists('remote'): - vxlan['remote'] = conf.return_value('remote') - - # Remote port of VXLAN tunnel - if conf.exists('port'): - vxlan['remote_port'] = int(conf.return_value('port')) - - # Virtual Network Identifier - if conf.exists('vni'): - vxlan['vni'] = conf.return_value('vni') + # VXLAN is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + vxlan['mtu'] = '1450' return vxlan - def verify(vxlan): - if vxlan['deleted']: - if vxlan['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{vxlan["intf"]}" as it is a ' - f'member of bridge "{vxlan["is_bridge_member"]}"!')) - + if 'deleted' in vxlan: + verify_bridge_delete(vxlan) return None - if vxlan['mtu'] < 1500: + if int(vxlan['mtu']) < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - if vxlan['group']: - if not vxlan['source_interface']: + if 'group' in vxlan: + if 'source_interface' not in vxlan: raise ConfigError('Multicast VXLAN requires an underlaying interface ') - if not vxlan['source_interface'] in interfaces(): - raise ConfigError('VXLAN source interface does not exist') + verify_source_interface(vxlan) - if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): + if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): raise ConfigError('Group, remote or source-address must be configured') - if not vxlan['vni']: + if 'vni' not in vxlan: raise ConfigError('Must configure VNI for VXLAN') - if vxlan['source_interface']: + if 'source_interface' in vxlan: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) - if underlay_mtu < (vxlan['mtu'] + 50): + if underlay_mtu < (int(vxlan['mtu']) + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ - 'MTU is to small ({})'.format(underlay_mtu)) - - if ( vxlan['is_bridge_member'] - and ( vxlan['address'] - or vxlan['ipv6_eui64_prefix'] - or vxlan['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{vxlan["intf"]}" ' - f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!')) + f'MTU is to small ({underlay_mtu} bytes)') + verify_address(vxlan) return None @@ -221,73 +85,26 @@ def generate(vxlan): def apply(vxlan): # Check if the VXLAN interface already exists - if vxlan['intf'] in interfaces(): - v = VXLANIf(vxlan['intf']) + if vxlan['ifname'] in interfaces(): + v = VXLANIf(vxlan['ifname']) # VXLAN is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() - if not vxlan['deleted']: + if 'deleted' not in vxlan: # VXLAN interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig conf = deepcopy(VXLANIf.get_config()) # Assign VXLAN instance configuration parameters to config dict - conf['vni'] = vxlan['vni'] - conf['group'] = vxlan['group'] - conf['src_address'] = vxlan['source_address'] - conf['src_interface'] = vxlan['source_interface'] - conf['remote'] = vxlan['remote'] - conf['port'] = vxlan['remote_port'] + for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: + if tmp in vxlan: + conf[tmp] = vxlan[tmp] # Finally create the new interface - v = VXLANIf(vxlan['intf'], **conf) - # update interface description used e.g. by SNMP - v.set_alias(vxlan['description']) - # Maximum Transfer Unit (MTU) - v.set_mtu(vxlan['mtu']) - - # configure ARP cache timeout in milliseconds - v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) - # configure ARP filter configuration - v.set_arp_filter(vxlan['ip_disable_arp_filter']) - # configure ARP accept - v.set_arp_accept(vxlan['ip_enable_arp_accept']) - # configure ARP announce - v.set_arp_announce(vxlan['ip_enable_arp_announce']) - # configure ARP ignore - v.set_arp_ignore(vxlan['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - v.set_proxy_arp(vxlan['ip_proxy_arp']) - # IPv6 accept RA - v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra']) - # IPv6 address autoconfiguration - v.set_ipv6_autoconf(vxlan['ipv6_autoconf']) - # IPv6 forwarding - v.set_ipv6_forwarding(vxlan['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in vxlan['address']: - v.add_addr(addr) - - # IPv6 EUI-based addresses - for addr in vxlan['ipv6_eui64_prefix']: - v.add_ipv6_eui64_address(addr) - - # As the VXLAN interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not vxlan['disable']: - v.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if vxlan['is_bridge_member']: - v.add_to_bridge(vxlan['is_bridge_member']) + v = VXLANIf(vxlan['ifname'], **conf) + v.update(vxlan) return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index c24c9a7ce..8b64cde4d 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -15,308 +15,98 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from sys import exit from copy import deepcopy -from netifaces import interfaces from vyos.config import Config -from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete from vyos.ifconfig import WireGuardIf -from vyos.util import chown, chmod_750, call -from vyos.validate import is_member, is_ipv6 +from vyos.util import check_kmod from vyos import ConfigError - from vyos import airbag airbag.enable() -kdir = r'/config/auth/wireguard' - -default_config_data = { - 'intfc': '', - 'address': [], - 'address_remove': [], - 'description': '', - 'listen_port': '', - 'deleted': False, - 'disable': False, - 'fwmark': 0, - 'is_bridge_member': False, - 'mtu': 1420, - 'peer': [], - 'peer_remove': [], # stores public keys of peers to remove - 'pk': f'{kdir}/default/private.key', - 'vrf': '' -} - -def _check_kmod(): - modules = ['wireguard'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - - -def _migrate_default_keys(): - if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): - location = f'{kdir}/default' - if not os.path.exists(location): - os.makedirs(location) - - chown(location, 'root', 'vyattacfg') - chmod_750(location) - os.rename(f'{kdir}/private.key', f'{location}/private.key') - os.rename(f'{kdir}/public.key', f'{location}/public.key') - - def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() base = ['interfaces', 'wireguard'] - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - wg = deepcopy(default_config_data) - wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists(base + [wg['intf']]): - wg['deleted'] = True - return wg - - conf.set_level(base + [wg['intf']]) - - # retrieve configured interface addresses - if conf.exists(['address']): - wg['address'] = conf.return_values(['address']) - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values(['address']) - wg['address_remove'] = list_diff(eff_addr, wg['address']) - - # retrieve interface description - if conf.exists(['description']): - wg['description'] = conf.return_value(['description']) - - # disable interface - if conf.exists(['disable']): - wg['disable'] = True - - # local port to listen on - if conf.exists(['port']): - wg['listen_port'] = conf.return_value(['port']) - - # fwmark value - if conf.exists(['fwmark']): - wg['fwmark'] = int(conf.return_value(['fwmark'])) - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - wg['mtu'] = int(conf.return_value(['mtu'])) - - # retrieve VRF instance - if conf.exists('vrf'): - wg['vrf'] = conf.return_value('vrf') - - # private key - if conf.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, conf.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = conf.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) - for peer in peer_rem: - wg['peer_remove'].append( - conf.return_effective_value(['peer', peer, 'pubkey'])) - - # peer settings - if conf.exists(['peer']): - for p in conf.list_nodes(['peer']): - # set new config level for this peer - conf.set_level(base + [wg['intf'], 'peer', p]) - peer = { - 'allowed-ips': [], - 'address': '', - 'name': p, - 'persistent_keepalive': '', - 'port': '', - 'psk': '', - 'pubkey': '' - } - - # peer allowed-ips - if conf.exists(['allowed-ips']): - peer['allowed-ips'] = conf.return_values(['allowed-ips']) - - # peer address - if conf.exists(['address']): - peer['address'] = conf.return_value(['address']) - - # peer port - if conf.exists(['port']): - peer['port'] = conf.return_value(['port']) - - # persistent-keepalive - if conf.exists(['persistent-keepalive']): - peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) - - # preshared-key - if conf.exists(['preshared-key']): - peer['psk'] = conf.return_value(['preshared-key']) - - # peer pubkeys - if conf.exists(['pubkey']): - key_eff = conf.return_effective_value(['pubkey']) - key_cfg = conf.return_value(['pubkey']) - peer['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - if conf.exists(['disable']): - wg['peer_remove'].append(peer['pubkey']) - else: - wg['peer'].append(peer) - - return wg - - -def verify(wg): - if wg['deleted']: - if wg['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{wg["intf"]}" as it is a member ' - f'of bridge "{wg["is_bridge_member"]}"!')) - + wireguard = get_interface_dict(conf, base) + + # Wireguard is "special" the default MTU is 1420 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + wireguard['mtu'] = '1420' + + # Mangle private key - it has a default so its always valid + wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard) + + # Determine which Wireguard peer has been removed. + # Peers can only be removed with their public key! + tmp = node_changed(conf, ['peer']) + if tmp: + dict = {} + for peer in tmp: + peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey']) + dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict) + wireguard.update(dict) + + return wireguard + +def verify(wireguard): + if 'deleted' in wireguard: + verify_bridge_delete(wireguard) return None - if wg['is_bridge_member'] and wg['address']: - raise ConfigError(( - f'Cannot assign address to interface "{wg["intf"]}" ' - f'as it is a member of bridge "{wg["is_bridge_member"]}"!')) + verify_address(wireguard) + verify_vrf(wireguard) - if wg['vrf']: - if wg['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{wg["vrf"]}" does not exist') + if not os.path.exists(wireguard['private_key']): + raise ConfigError('Wireguard private-key not found! Execute: ' \ + '"run generate wireguard [default-keypair|named-keypairs]"') - if wg['is_bridge_member']: - raise ConfigError(( - f'Interface "{wg["intf"]}" cannot be member of VRF ' - f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} ' - f'at the same time!')) + if 'address' not in wireguard: + raise ConfigError('IP address required!') - if not os.path.exists(wg['pk']): - raise ConfigError('No keys found, generate them by executing:\n' \ - '"run generate wireguard [keypair|named-keypairs]"') - - if not wg['address']: - raise ConfigError(f'IP address required for interface "{wg["intf"]}"!') - - if not wg['peer']: - raise ConfigError(f'Peer required for interface "{wg["intf"]}"!') + if 'peer' not in wireguard: + raise ConfigError('At least one Wireguard peer is required!') # run checks on individual configured WireGuard peer - for peer in wg['peer']: - if not peer['allowed-ips']: - raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!') - - if not peer['pubkey']: - raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!') + for tmp in wireguard['peer']: + peer = wireguard['peer'][tmp] - if peer['address'] and not peer['port']: - raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!') + if 'allowed_ips' not in peer: + raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') - if not peer['address'] and peer['port']: - raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!') + if 'pubkey' not in peer: + raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') + if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): + raise ConfigError('Both Wireguard port and address must be defined ' + f'for peer "{tmp}" if either one of them is set!') -def apply(wg): - # init wg class - w = WireGuardIf(wg['intf']) - - # single interface removal - if wg['deleted']: - w.remove() +def apply(wireguard): + if 'deleted' in wireguard: + WireGuardIf(wireguard['ifname']).remove() return None - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in wg['address_remove']: - w.del_addr(addr) - for addr in wg['address']: - w.add_addr(addr) - - # Maximum Transmission Unit (MTU) - w.set_mtu(wg['mtu']) - - # update interface description used e.g. within SNMP - w.set_alias(wg['description']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not wg['is_bridge_member']: - w.set_vrf(wg['vrf']) - - # remove peers - for pub_key in wg['peer_remove']: - w.remove_peer(pub_key) - - # peer pubkey - # setting up the wg interface - w.config['private_key'] = c['pk'] - - for peer in wg['peer']: - # peer pubkey - w.config['pubkey'] = peer['pubkey'] - # peer allowed-ips - w.config['allowed-ips'] = peer['allowed-ips'] - # local listen port - if wg['listen_port']: - w.config['port'] = wg['listen_port'] - # fwmark - if c['fwmark']: - w.config['fwmark'] = wg['fwmark'] - - # endpoint - if peer['address'] and peer['port']: - if is_ipv6(peer['address']): - w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port']) - else: - w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port']) - - # persistent-keepalive - if peer['persistent_keepalive']: - w.config['keepalive'] = peer['persistent_keepalive'] - - if peer['psk']: - w.config['psk'] = peer['psk'] - - w.update() - - # Enable/Disable interface - if wg['disable']: - w.set_admin_state('down') - else: - w.set_admin_state('up') - + w = WireGuardIf(wireguard['ifname']) + w.update(wireguard) return None if __name__ == '__main__': try: - _check_kmod() - _migrate_default_keys() + check_kmod('wireguard') c = get_config() verify(c) apply(c) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 0162b642c..b6f247952 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -15,497 +15,163 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os + from sys import exit from re import findall - from copy import deepcopy - -from netifaces import interfaces from netaddr import EUI, mac_unix_expanded from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import WiFiIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import dict_merge +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import WiFiIf from vyos.template import render -from vyos.util import chown, call -from vyos.validate import is_member +from vyos.util import call from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'cap_ht' : False, - 'cap_ht_40mhz_incapable' : False, - 'cap_ht_powersave' : False, - 'cap_ht_chan_set_width' : '', - 'cap_ht_delayed_block_ack' : False, - 'cap_ht_dsss_cck_40' : False, - 'cap_ht_greenfield' : False, - 'cap_ht_ldpc' : False, - 'cap_ht_lsig_protection' : False, - 'cap_ht_max_amsdu' : '', - 'cap_ht_short_gi' : [], - 'cap_ht_smps' : '', - 'cap_ht_stbc_rx' : '', - 'cap_ht_stbc_tx' : False, - 'cap_req_ht' : False, - 'cap_req_vht' : False, - 'cap_vht' : False, - 'cap_vht_antenna_cnt' : '', - 'cap_vht_antenna_fixed' : False, - 'cap_vht_beamform' : '', - 'cap_vht_center_freq_1' : '', - 'cap_vht_center_freq_2' : '', - 'cap_vht_chan_set_width' : '', - 'cap_vht_ldpc' : False, - 'cap_vht_link_adaptation' : '', - 'cap_vht_max_mpdu_exp' : '', - 'cap_vht_max_mpdu' : '', - 'cap_vht_short_gi' : [], - 'cap_vht_stbc_rx' : '', - 'cap_vht_stbc_tx' : False, - 'cap_vht_tx_powersave' : False, - 'cap_vht_vht_cf' : False, - 'channel': '', - 'country_code': '', - 'deleted': False, - 'disable_broadcast_ssid' : False, - 'disable_link_detect' : 1, - 'expunge_failing_stations' : False, - 'hw_id' : '', - 'intf': '', - 'isolate_stations' : False, - 'max_stations' : '', - 'mgmt_frame_protection' : 'disabled', - 'mode' : 'g', - 'phy' : '', - 'reduce_transmit_power' : '', - 'sec_wep' : False, - 'sec_wep_key' : [], - 'sec_wpa' : False, - 'sec_wpa_cipher' : [], - 'sec_wpa_mode' : 'both', - 'sec_wpa_passphrase' : '', - 'sec_wpa_radius' : [], - 'ssid' : '', - 'op_mode' : 'monitor', - 'vif': {}, - 'vif_remove': [], - 'vif_s': {}, - 'vif_s_remove': [] -} - # XXX: wpa_supplicant works on the source interface -wpa_suppl_conf = '/run/wpa_supplicant/{intf}.conf' -hostapd_conf = '/run/hostapd/{intf}.conf' +wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' +hostapd_conf = '/run/hostapd/{ifname}.conf' + +def find_other_stations(conf, base, ifname): + """ + Only one wireless interface per phy can be in station mode - + find all interfaces attached to a phy which run in station mode + """ + old_level = conf.get_level() + conf.set_level(base) + dict = {} + for phy in os.listdir('/sys/class/ieee80211'): + list = [] + for interface in conf.list_nodes([]): + if interface == ifname: + continue + # the following node is mandatory + if conf.exists([interface, 'physical-device', phy]): + tmp = conf.return_value([interface, 'type']) + if tmp == 'station': + list.append(interface) + if list: + dict.update({phy: list}) + conf.set_level(old_level) + return dict def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # check if wireless interface has been removed - cfg_base = ['interfaces', 'wireless ', ifname] - if not conf.exists(cfg_base): - wifi = deepcopy(default_config_data) - wifi['intf'] = ifname - wifi['deleted'] = True - # we need to know if we're a bridge member so we can refuse deletion - wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge') - # we can not bail out early as wireless interface can not be removed - # Kernel will complain with: RTNETLINK answers: Operation not supported. - # Thus we need to remove individual settings - return wifi - - # set new configuration level - conf.set_level(cfg_base) - - # get common interface settings - wifi, disabled = intf_to_dict(conf, default_config_data) - - # 40MHz intolerance, use 20MHz only - if conf.exists('capabilities ht 40mhz-incapable'): - wifi['cap_ht'] = True - wifi['cap_ht_40mhz_incapable'] = True - - # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] - if conf.exists('capabilities ht auto-powersave'): - wifi['cap_ht'] = True - wifi['cap_ht_powersave'] = True - - # Supported channel set width - if conf.exists('capabilities ht channel-set-width'): - wifi['cap_ht'] = True - wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width') - - # HT-delayed Block Ack - if conf.exists('capabilities ht delayed-block-ack'): - wifi['cap_ht'] = True - wifi['cap_ht_delayed_block_ack'] = True - - # DSSS/CCK Mode in 40 MHz - if conf.exists('capabilities ht dsss-cck-40'): - wifi['cap_ht'] = True - wifi['cap_ht_dsss_cck_40'] = True - - # HT-greenfield capability - if conf.exists('capabilities ht greenfield'): - wifi['cap_ht'] = True - wifi['cap_ht_greenfield'] = True - - # LDPC coding capability - if conf.exists('capabilities ht ldpc'): - wifi['cap_ht'] = True - wifi['cap_ht_ldpc'] = True - - # L-SIG TXOP protection capability - if conf.exists('capabilities ht lsig-protection'): - wifi['cap_ht'] = True - wifi['cap_ht_lsig_protection'] = True - - # Set Maximum A-MSDU length - if conf.exists('capabilities ht max-amsdu'): - wifi['cap_ht'] = True - wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu') - - # Short GI capabilities - if conf.exists('capabilities ht short-gi'): - wifi['cap_ht'] = True - wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi') - - # Spatial Multiplexing Power Save (SMPS) settings - if conf.exists('capabilities ht smps'): - wifi['cap_ht'] = True - wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps') - - # Support for receiving PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities ht stbc rx'): - wifi['cap_ht'] = True - wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx') - - # Support for sending PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities ht stbc tx'): - wifi['cap_ht'] = True - wifi['cap_ht_stbc_tx'] = True - - # Require stations to support HT PHY (reject association if they do not) - if conf.exists('capabilities require-ht'): - wifi['cap_req_ht'] = True - - # Require stations to support VHT PHY (reject association if they do not) - if conf.exists('capabilities require-vht'): - wifi['cap_req_vht'] = True - - # Number of antennas on this card - if conf.exists('capabilities vht antenna-count'): - wifi['cap_vht'] = True - wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count') - - # set if antenna pattern does not change during the lifetime of an association - if conf.exists('capabilities vht antenna-pattern-fixed'): - wifi['cap_vht'] = True - wifi['cap_vht_antenna_fixed'] = True - - # Beamforming capabilities - if conf.exists('capabilities vht beamform'): - wifi['cap_vht'] = True - wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform') - - # VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes) - if conf.exists('capabilities vht center-channel-freq freq-1'): - wifi['cap_vht'] = True - wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1') - - # VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode) - if conf.exists('capabilities vht center-channel-freq freq-2'): - wifi['cap_vht'] = True - wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2') - - # VHT operating Channel width - if conf.exists('capabilities vht channel-set-width'): - wifi['cap_vht'] = True - wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width') - - # LDPC coding capability - if conf.exists('capabilities vht ldpc'): - wifi['cap_vht'] = True - wifi['cap_vht_ldpc'] = True - - # VHT link adaptation capabilities - if conf.exists('capabilities vht link-adaptation'): - wifi['cap_vht'] = True - wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation') - - # Set the maximum length of A-MPDU pre-EOF padding that the station can receive - if conf.exists('capabilities vht max-mpdu-exp'): - wifi['cap_vht'] = True - wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp') - - # Increase Maximum MPDU length - if conf.exists('capabilities vht max-mpdu'): - wifi['cap_vht'] = True - wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu') - - # Increase Maximum MPDU length - if conf.exists('capabilities vht short-gi'): - wifi['cap_vht'] = True - wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi') - - # Support for receiving PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities vht stbc rx'): - wifi['cap_vht'] = True - wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx') - - # Support for the transmission of at least 2x1 STBC (Space Time Block Coding) - if conf.exists('capabilities vht stbc tx'): - wifi['cap_vht'] = True - wifi['cap_vht_stbc_tx'] = True - - # Support for VHT TXOP Power Save Mode - if conf.exists('capabilities vht tx-powersave'): - wifi['cap_vht'] = True - wifi['cap_vht_tx_powersave'] = True - - # STA supports receiving a VHT variant HT Control field - if conf.exists('capabilities vht vht-cf'): - wifi['cap_vht'] = True - wifi['cap_vht_vht_cf'] = True - - # Wireless radio channel - if conf.exists('channel'): - wifi['channel'] = conf.return_value('channel') - - # Disable broadcast of SSID from access-point - if conf.exists('disable-broadcast-ssid'): - wifi['disable_broadcast_ssid'] = True - - # Disassociate stations based on excessive transmission failures - if conf.exists('expunge-failing-stations'): - wifi['expunge_failing_stations'] = True - - # retrieve real hardware address - if conf.exists('hw-id'): - wifi['hw_id'] = conf.return_value('hw-id') - - # Isolate stations on the AP so they cannot see each other - if conf.exists('isolate-stations'): - wifi['isolate_stations'] = True - - # Wireless physical device - if conf.exists('physical-device'): - wifi['phy'] = conf.return_value('physical-device') - - # Maximum number of wireless radio stations - if conf.exists('max-stations'): - wifi['max_stations'] = conf.return_value('max-stations') - - # Management Frame Protection (MFP) according to IEEE 802.11w - if conf.exists('mgmt-frame-protection'): - wifi['mgmt_frame_protection'] = conf.return_value('mgmt-frame-protection') - - # Wireless radio mode - if conf.exists('mode'): - wifi['mode'] = conf.return_value('mode') - - # Transmission power reduction in dBm - if conf.exists('reduce-transmit-power'): - wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power') - - # WEP enabled? - if conf.exists('security wep'): - wifi['sec_wep'] = True - - # WEP encryption key(s) - if conf.exists('security wep key'): - wifi['sec_wep_key'] = conf.return_values('security wep key') - - # WPA enabled? - if conf.exists('security wpa'): - wifi['sec_wpa'] = True - - # WPA Cipher suite - if conf.exists('security wpa cipher'): - wifi['sec_wpa_cipher'] = conf.return_values('security wpa cipher') - - # WPA mode - if conf.exists('security wpa mode'): - wifi['sec_wpa_mode'] = conf.return_value('security wpa mode') - - # WPA default ciphers depend on WPA mode - if not wifi['sec_wpa_cipher']: - if wifi['sec_wpa_mode'] == 'wpa': - wifi['sec_wpa_cipher'].append('TKIP') - wifi['sec_wpa_cipher'].append('CCMP') - - elif wifi['sec_wpa_mode'] == 'wpa2': - wifi['sec_wpa_cipher'].append('CCMP') - - elif wifi['sec_wpa_mode'] == 'both': - wifi['sec_wpa_cipher'].append('CCMP') - wifi['sec_wpa_cipher'].append('TKIP') - - # WPA Group Cipher suite - if conf.exists('security wpa group-cipher'): - wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher') - - # WPA personal shared pass phrase - if conf.exists('security wpa passphrase'): - wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') - - # WPA RADIUS source address - if conf.exists('security wpa radius source-address'): - wifi['sec_wpa_radius_source'] = conf.return_value('security wpa radius source-address') - - # WPA RADIUS server - for server in conf.list_nodes('security wpa radius server'): - # set new configuration level - conf.set_level(cfg_base + ' security wpa radius server ' + server) - radius = { - 'server' : server, - 'acc_port' : '', - 'disabled': False, - 'port' : 1812, - 'key' : '' - } - - # RADIUS server port - if conf.exists('port'): - radius['port'] = int(conf.return_value('port')) - - # receive RADIUS accounting info - if conf.exists('accounting'): - radius['acc_port'] = radius['port'] + 1 - - # Check if RADIUS server was temporary disabled - if conf.exists(['disable']): - radius['disabled'] = True - - # RADIUS server shared-secret - if conf.exists('key'): - radius['key'] = conf.return_value('key') - - # append RADIUS server to list of servers - wifi['sec_wpa_radius'].append(radius) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - - # Wireless access-point service set identifier (SSID) - if conf.exists('ssid'): - wifi['ssid'] = conf.return_value('ssid') - - # Wireless device type for this interface - if conf.exists('type'): - tmp = conf.return_value('type') - if tmp == 'access-point': - tmp = 'ap' - - wifi['op_mode'] = tmp + base = ['interfaces', 'wireless'] + wifi = get_interface_dict(conf, base) + + if 'security' in wifi and 'wpa' in wifi['security']: + wpa_cipher = wifi['security']['wpa'].get('cipher') + wpa_mode = wifi['security']['wpa'].get('mode') + if not wpa_cipher: + tmp = None + if wpa_mode == 'wpa': + tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}} + elif wpa_mode == 'wpa2': + tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}} + elif wpa_mode == 'both': + tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}} + + if tmp: wifi = dict_merge(tmp, wifi) # retrieve configured regulatory domain - conf.set_level('system') - if conf.exists('wifi-regulatory-domain'): - wifi['country_code'] = conf.return_value('wifi-regulatory-domain') + conf.set_level(['system']) + if conf.exists(['wifi-regulatory-domain']): + wifi['country_code'] = conf.return_value(['wifi-regulatory-domain']) - return wifi + # Only one wireless interface per phy can be in station mode + tmp = find_other_stations(conf, base, wifi['ifname']) + if tmp: wifi['station_interfaces'] = tmp + return wifi def verify(wifi): - if wifi['deleted']: - if wifi['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{wifi["intf"]}" as it is a ' - f'member of bridge "{wifi["is_bridge_member"]}"!')) - + if 'deleted' in wifi: + verify_bridge_delete(wifi) return None - if wifi['op_mode'] != 'monitor' and not wifi['ssid']: - raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) + if 'physical_device' not in wifi: + raise ConfigError('You must specify a physical-device "phy"') - if not wifi['phy']: - raise ConfigError('You must specify physical-device') - - if not wifi['mode']: + if 'type' not in wifi: raise ConfigError('You must specify a WiFi mode') - if wifi['op_mode'] == 'ap': - c = Config() - if not c.exists('system wifi-regulatory-domain'): - raise ConfigError('Wireless regulatory domain is mandatory,\n' \ - 'use "set system wifi-regulatory-domain".') - - if not wifi['channel']: - raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - - if len(wifi['sec_wep_key']) > 4: - raise ConfigError('No more then 4 WEP keys configurable') + if 'ssid' not in wifi and wifi['type'] != 'monitor': + raise ConfigError('SSID must be configured') - if wifi['cap_vht'] and not wifi['cap_ht']: - raise ConfigError('Specify HT flags if you want to use VHT!') - - if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: - raise ConfigError('Cannot use beam forming with just one antenna!') - - if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3: - # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 - raise ConfigError('Single-user beam former requires at least 3 antennas!') - - if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): - raise ConfigError('Missing WEP keys') - - if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']): - raise ConfigError('Misssing WPA key or RADIUS server') - - for radius in wifi['sec_wpa_radius']: - if not radius['key']: - raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) - - if ( wifi['is_bridge_member'] - and ( wifi['address'] - or wifi['ipv6_eui64_prefix'] - or wifi['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{wifi["intf"]}" ' - f'as it is a member of bridge "{wifi["is_bridge_member"]}"!')) - - if wifi['vrf']: - if wifi['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist') - - if wifi['is_bridge_member']: - raise ConfigError(( - f'Interface "{wifi["intf"]}" cannot be member of VRF ' - f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} ' - f'at the same time!')) + if wifi['type'] == 'access-point': + if 'country_code' not in wifi: + raise ConfigError('Wireless regulatory domain is mandatory,\n' \ + 'use "set system wifi-regulatory-domain" for configuration.') + + if 'channel' not in wifi: + raise ConfigError('Wireless channel must be configured!') + + if 'security' in wifi: + if {'wep', 'wpa'} <= set(wifi.get('security', {})): + raise ConfigError('Must either use WEP or WPA security!') + + if 'wep' in wifi['security']: + if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4: + raise ConfigError('No more then 4 WEP keys configurable') + elif 'key' not in wifi['security']['wep']: + raise ConfigError('Security WEP configured - missing WEP keys!') + + elif 'wpa' in wifi['security']: + wpa = wifi['security']['wpa'] + if not any(i in ['passphrase', 'radius'] for i in wpa): + raise ConfigError('Misssing WPA key or RADIUS server') + + if 'radius' in wpa: + if 'server' in wpa['radius']: + for server in wpa['radius']['server']: + if 'key' not in wpa['radius']['server'][server]: + raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}') + + if 'capabilities' in wifi: + capabilities = wifi['capabilities'] + if 'vht' in capabilities: + if 'ht' not in capabilities: + raise ConfigError('Specify HT flags if you want to use VHT!') + + if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})): + if capabilities['vht']['antenna_count'] == '1': + raise ConfigError('Cannot use beam forming with just one antenna!') + + if capabilities['vht']['beamform'] == 'single-user-beamformer': + if int(capabilities['vht']['antenna_count']) < 3: + # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 + raise ConfigError('Single-user beam former requires at least 3 antennas!') + + if 'station_interfaces' in wifi and wifi['type'] == 'station': + phy = wifi['physical_device'] + if phy in wifi['station_interfaces']: + if len(wifi['station_interfaces'][phy]) > 0: + raise ConfigError('Only one station per wireless physical interface possible!') + + verify_address(wifi) + verify_vrf(wifi) # use common function to verify VLAN configuration verify_vlan_config(wifi) - conf = Config() - # Only one wireless interface per phy can be in station mode - base = ['interfaces', 'wireless'] - for phy in os.listdir('/sys/class/ieee80211'): - stations = [] - for wlan in conf.list_nodes(base): - # the following node is mandatory - if conf.exists(base + [wlan, 'physical-device', phy]): - tmp = conf.return_value(base + [wlan, 'type']) - if tmp == 'station': - stations.append(wlan) - - if len(stations) > 1: - raise ConfigError('Only one station per wireless physical interface possible!') - return None def generate(wifi): - interface = wifi['intf'] + interface = wifi['ifname'] # always stop hostapd service first before reconfiguring it call(f'systemctl stop hostapd@{interface}.service') @@ -513,7 +179,7 @@ def generate(wifi): call(f'systemctl stop wpa_supplicant@{interface}.service') # Delete config files if interface is removed - if wifi['deleted']: + if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): os.unlink(hostapd_conf.format(**wifi)) @@ -522,10 +188,10 @@ def generate(wifi): return None - if not wifi['mac']: + if 'mac' not in wifi: # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd # generate locally administered MAC address from used phy interface - with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f: + with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f: # some PHYs tend to have multiple interfaces and thus supply multiple MAC # addresses - we only need the first one for our calculation tmp = f.readline().rstrip() @@ -545,20 +211,18 @@ def generate(wifi): wifi['mac'] = str(mac) # render appropriate new config files depending on access-point or station mode - if wifi['op_mode'] == 'ap': - render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi) + if wifi['type'] == 'access-point': + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) - elif wifi['op_mode'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi) + elif wifi['type'] == 'station': + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True) return None def apply(wifi): - interface = wifi['intf'] - if wifi['deleted']: - w = WiFiIf(interface) - # delete interface - w.remove() + interface = wifi['ifname'] + if 'deleted' in wifi: + WiFiIf(interface).remove() else: # WiFi interface needs to be created on-block (e.g. mode or physical # interface) instead of passing a ton of arguments, I just use a dict @@ -566,97 +230,21 @@ def apply(wifi): conf = deepcopy(WiFiIf.get_config()) # Assign WiFi instance configuration parameters to config dict - conf['phy'] = wifi['phy'] + conf['phy'] = wifi['physical_device'] # Finally create the new interface w = WiFiIf(interface, **conf) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not wifi['is_bridge_member']: - w.set_vrf(wifi['vrf']) - - # update interface description used e.g. within SNMP - w.set_alias(wifi['description']) - - if wifi['dhcp_client_id']: - w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id'] - - if wifi['dhcp_hostname']: - w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname'] - - if wifi['dhcp_vendor_class_id']: - w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id'] - - if wifi['dhcpv6_prm_only']: - w.dhcp.v6.options['dhcpv6_prm_only'] = True - - if wifi['dhcpv6_temporary']: - w.dhcp.v6.options['dhcpv6_temporary'] = True - - if wifi['dhcpv6_pd_length']: - w.dhcp.v6.options['dhcpv6_pd_length'] = wifi['dhcpv6_pd_length'] - - if wifi['dhcpv6_pd_interfaces']: - w.dhcp.v6.options['dhcpv6_pd_interfaces'] = wifi['dhcpv6_pd_interfaces'] - - # ignore link state changes - w.set_link_detect(wifi['disable_link_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in wifi['ipv6_eui64_prefix_remove']: - w.del_ipv6_eui64_address(addr) - - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed - if wifi['mac']: - w.set_mac(wifi['mac']) - elif wifi['hw_id']: - w.set_mac(wifi['hw_id']) - - # Add IPv6 EUI-based addresses - for addr in wifi['ipv6_eui64_prefix']: - w.add_ipv6_eui64_address(addr) - - # configure ARP filter configuration - w.set_arp_filter(wifi['ip_disable_arp_filter']) - # configure ARP accept - w.set_arp_accept(wifi['ip_enable_arp_accept']) - # configure ARP announce - w.set_arp_announce(wifi['ip_enable_arp_announce']) - # configure ARP ignore - w.set_arp_ignore(wifi['ip_enable_arp_ignore']) - # IPv6 accept RA - w.set_ipv6_accept_ra(wifi['ipv6_accept_ra']) - # IPv6 address autoconfiguration - w.set_ipv6_autoconf(wifi['ipv6_autoconf']) - # IPv6 forwarding - w.set_ipv6_forwarding(wifi['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect']) - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in wifi['address_remove']: - w.del_addr(addr) - for addr in wifi['address']: - w.add_addr(addr) - - # apply all vlans to interface - apply_all_vlans(w, wifi) + w.update(wifi) # Enable/Disable interface - interface is always placed in # administrative down state in WiFiIf class - if not wifi['disable']: - w.set_admin_state('up') - + if 'disable' not in wifi: # Physical interface is now configured. Proceed by starting hostapd or # wpa_supplicant daemon. When type is monitor we can just skip this. - if wifi['op_mode'] == 'ap': + if wifi['type'] == 'access-point': call(f'systemctl start hostapd@{interface}.service') - elif wifi['op_mode'] == 'station': + elif wifi['type'] == 'station': call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index ec5a85e54..4081be3c9 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -20,21 +20,16 @@ from fnmatch import fnmatch from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.template import render from vyos.util import call -from vyos.xml import defaults +from vyos.util import check_kmod from vyos import ConfigError from vyos import airbag airbag.enable() -def check_kmod(): - modules = ['option', 'usb_wwan', 'usbserial'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') +k_mod = ['option', 'usb_wwan', 'usbserial'] def find_device_file(device): """ Recurively search /dev for the given device file and return its full path. @@ -47,44 +42,23 @@ def find_device_file(device): return None def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values base = ['interfaces', 'wirelessmodem'] - default_values = defaults(base) - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] - - wwan = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if wwan == {}: - wwan.update({'deleted' : ''}) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - wwan = dict_merge(default_values, wwan) - - # Add interface instance name into dictionary - wwan.update({'ifname': ifname}) - + wwan = get_interface_dict(conf, base) return wwan def verify(wwan): - if 'deleted' in wwan.keys(): + if 'deleted' in wwan: return None - if not 'apn' in wwan.keys(): + if not 'apn' in wwan: raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) - if not 'device' in wwan.keys(): + if not 'device' in wwan: raise ConfigError('Physical "device" must be configured') # we can not use isfile() here as Linux device files are no regular files @@ -141,11 +115,11 @@ def generate(wwan): return None def apply(wwan): - if 'deleted' in wwan.keys(): + if 'deleted' in wwan: # bail out early return None - if not 'disable' in wwan.keys(): + if not 'disable' in wwan: # "dial" WWAN connection call('systemctl start ppp@{ifname}.service'.format(**wwan)) @@ -153,7 +127,7 @@ def apply(wwan): if __name__ == '__main__': try: - check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index b0a029f2b..dd34dfd66 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -24,13 +24,17 @@ from netifaces import interfaces from vyos.config import Config from vyos.template import render -from vyos.util import call, cmd +from vyos.util import call +from vyos.util import cmd +from vyos.util import check_kmod from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() +k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] + default_config_data = { 'deleted': False, 'destination': [], @@ -44,15 +48,6 @@ default_config_data = { iptables_nat_config = '/tmp/vyos-nat-rules.nft' -def _check_kmod(): - """ load required Kernel modules """ - modules = ['nft_nat', 'nft_chain_nat_ipv4'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - - def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. Handler is required when adding NAT/Conntrack helper targets """ @@ -79,9 +74,6 @@ def verify_rule(rule, err_msg): 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') - if not rule['translation_address']: - raise ConfigError(f'{err_msg} translation address not specified') - def parse_configuration(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ @@ -228,10 +220,10 @@ def verify(nat): for rule in nat['source']: interface = rule['interface_out'] - err_msg = f"Source NAT configuration error in rule {rule['number']}:" + err_msg = f'Source NAT configuration error in rule "{rule["number"]}":' - if interface and interface not in interfaces(): - print(f'NAT configuration warning: interface {interface} does not exist on this system') + if interface and interface not in 'any' and interface not in interfaces(): + print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') if not rule['interface_out']: raise ConfigError(f'{err_msg} outbound-interface not specified') @@ -246,10 +238,10 @@ def verify(nat): for rule in nat['destination']: interface = rule['interface_in'] - err_msg = f"Destination NAT configuration error in rule {rule['number']}:" + err_msg = f'Destination NAT configuration error in rule "{rule["number"]}":' - if interface and interface not in interfaces(): - print(f'NAT configuration warning: interface {interface} does not exist on this system') + if interface and interface not in 'any' and interface not in interfaces(): + print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') if not rule['interface_in']: raise ConfigError(f'{err_msg} inbound-interface not specified') @@ -261,7 +253,6 @@ def verify(nat): def generate(nat): render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) - return None def apply(nat): @@ -273,7 +264,7 @@ def apply(nat): if __name__ == '__main__': try: - _check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index b539da98e..553cc2e97 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -147,17 +147,21 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/service_mdns-repeater.py index b43f9bdd8..1a6b2c328 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -17,69 +17,51 @@ import os from sys import exit -from copy import deepcopy -from netifaces import ifaddresses, AF_INET +from netifaces import ifaddresses, interfaces, AF_INET from vyos.config import Config -from vyos import ConfigError -from vyos.util import call from vyos.template import render - +from vyos.util import call +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/default/mdns-repeater' -default_config_data = { - 'disabled': False, - 'interfaces': [] -} - def get_config(): - mdns = deepcopy(default_config_data) conf = Config() base = ['service', 'mdns', 'repeater'] - if not conf.exists(base): - return None - else: - conf.set_level(base) - - # Service can be disabled by user - if conf.exists(['disable']): - mdns['disabled'] = True - return mdns - - # Interface to repeat mDNS advertisements - if conf.exists(['interface']): - mdns['interfaces'] = conf.return_values(['interface']) - + mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return mdns def verify(mdns): - if mdns is None: + if not mdns: return None - if mdns['disabled']: + if 'disable' in mdns: return None # We need at least two interfaces to repeat mDNS advertisments - if len(mdns['interfaces']) < 2: + if 'interface' not in mdns or len(mdns['interface']) < 2: raise ConfigError('mDNS repeater requires at least 2 configured interfaces!') # For mdns-repeater to work it is essential that the interfaces has # an IPv4 address assigned - for interface in mdns['interfaces']: - if AF_INET in ifaddresses(interface).keys(): - if len(ifaddresses(interface)[AF_INET]) < 1: - raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) + for interface in mdns['interface']: + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist!') + + if AF_INET not in ifaddresses(interface): + raise ConfigError('mDNS repeater requires an IPv4 address to be ' + f'configured on interface "{interface}"') return None def generate(mdns): - if mdns is None: + if not mdns: return None - if mdns['disabled']: + if 'disable' in mdns: print('Warning: mDNS repeater will be deactivated because it is disabled') return None @@ -87,7 +69,7 @@ def generate(mdns): return None def apply(mdns): - if (mdns is None) or mdns['disabled']: + if not mdns or 'disable' in mdns: call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 3149bbb2f..a8357f653 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -242,7 +242,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -253,6 +254,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index ef6148ebd..4e1c432ab 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -16,145 +16,85 @@ import os -from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +from vyos.configdict import dict_merge from vyos.template import render - +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/run/radvd/radvd.conf' -default_config_data = { - 'interfaces': [] -} - def get_config(): - rtradv = default_config_data conf = Config() - base_level = ['service', 'router-advert'] - - if not conf.exists(base_level): - return rtradv - - for interface in conf.list_nodes(base_level + ['interface']): - intf = { - 'name': interface, - 'hop_limit' : '64', - 'default_lifetime': '', - 'default_preference': 'medium', - 'dnssl': [], - 'link_mtu': '', - 'managed_flag': 'off', - 'interval_max': '600', - 'interval_min': '', - 'name_server': [], - 'other_config_flag': 'off', - 'prefixes' : [], - 'reachable_time': '0', - 'retrans_timer': '0', - 'send_advert': 'on' - } - - # set config level first to reduce boilerplate code - conf.set_level(base_level + ['interface', interface]) - - if conf.exists(['hop-limit']): - intf['hop_limit'] = conf.return_value(['hop-limit']) - - if conf.exists(['default-lifetime']): - intf['default_lifetime'] = conf.return_value(['default-lifetime']) - - if conf.exists(['default-preference']): - intf['default_preference'] = conf.return_value(['default-preference']) - - if conf.exists(['dnssl']): - intf['dnssl'] = conf.return_values(['dnssl']) - - if conf.exists(['link-mtu']): - intf['link_mtu'] = conf.return_value(['link-mtu']) - - if conf.exists(['managed-flag']): - intf['managed_flag'] = 'on' - - if conf.exists(['interval', 'max']): - intf['interval_max'] = conf.return_value(['interval', 'max']) - - if conf.exists(['interval', 'min']): - intf['interval_min'] = conf.return_value(['interval', 'min']) - - if conf.exists(['name-server']): - intf['name_server'] = conf.return_values(['name-server']) + base = ['service', 'router-advert'] + rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_interface_values = defaults(base + ['interface']) + # we deal with prefix defaults later on + if 'prefix' in default_interface_values: + del default_interface_values['prefix'] + + default_prefix_values = defaults(base + ['interface', 'prefix']) + + if 'interface' in rtradv: + for interface in rtradv['interface']: + rtradv['interface'][interface] = dict_merge( + default_interface_values, rtradv['interface'][interface]) + + if 'prefix' in rtradv['interface'][interface]: + for prefix in rtradv['interface'][interface]['prefix']: + rtradv['interface'][interface]['prefix'][prefix] = dict_merge( + default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) + + if 'name_server' in rtradv['interface'][interface]: + # always use a list when dealing with nameservers - eases the template generation + if isinstance(rtradv['interface'][interface]['name_server'], str): + rtradv['interface'][interface]['name_server'] = [ + rtradv['interface'][interface]['name_server']] - if conf.exists(['other-config-flag']): - intf['other_config_flag'] = 'on' - - if conf.exists(['reachable-time']): - intf['reachable_time'] = conf.return_value(['reachable-time']) - - if conf.exists(['retrans-timer']): - intf['retrans_timer'] = conf.return_value(['retrans-timer']) - - if conf.exists(['no-send-advert']): - intf['send_advert'] = 'off' - - for prefix in conf.list_nodes(['prefix']): - tmp = { - 'prefix' : prefix, - 'autonomous_flag' : 'on', - 'on_link' : 'on', - 'preferred_lifetime': 14400, - 'valid_lifetime' : 2592000 - - } - - # set config level first to reduce boilerplate code - conf.set_level(base_level + ['interface', interface, 'prefix', prefix]) - - if conf.exists(['no-autonomous-flag']): - tmp['autonomous_flag'] = 'off' - - if conf.exists(['no-on-link-flag']): - tmp['on_link'] = 'off' - - if conf.exists(['preferred-lifetime']): - tmp['preferred_lifetime'] = int(conf.return_value(['preferred-lifetime'])) + return rtradv - if conf.exists(['valid-lifetime']): - tmp['valid_lifetime'] = int(conf.return_value(['valid-lifetime'])) +def verify(rtradv): + if not rtradv: + return None - intf['prefixes'].append(tmp) + if 'interface' not in rtradv: + return None - rtradv['interfaces'].append(intf) + for interface in rtradv['interface']: + interface = rtradv['interface'][interface] + if 'prefix' in interface: + for prefix in interface['prefix']: + prefix = interface['prefix'][prefix] + valid_lifetime = prefix['valid_lifetime'] + if valid_lifetime == 'infinity': + valid_lifetime = 4294967295 - return rtradv + preferred_lifetime = prefix['preferred_lifetime'] + if preferred_lifetime == 'infinity': + preferred_lifetime = 4294967295 -def verify(rtradv): - for interface in rtradv['interfaces']: - for prefix in interface['prefixes']: - if not (prefix['valid_lifetime'] > prefix['preferred_lifetime']): - raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if not (int(valid_lifetime) > int(preferred_lifetime)): + raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') return None def generate(rtradv): - if not rtradv['interfaces']: + if not rtradv: return None - render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True) - - # adjust file permissions of new configuration file - if os.path.exists(config_file): - os.chmod(config_file, S_IRUSR | S_IWUSR | S_IRGRP) - + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644) return None def apply(rtradv): - if not rtradv['interfaces']: + if not rtradv: # bail out early - looks like removal from running config call('systemctl stop radvd.service') if os.path.exists(config_file): @@ -163,6 +103,7 @@ def apply(rtradv): return None call('systemctl restart radvd.service') + return None if __name__ == '__main__': diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index bafd26edc..e9806ef47 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -16,20 +16,16 @@ import os -from binascii import hexlify -from netifaces import interfaces -from time import sleep from sys import exit from vyos.config import Config from vyos.configverify import verify_vrf +from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.template import render +from vyos.util import call from vyos.validate import is_ipv4, is_addr_assigned from vyos.version import get_version_data -from vyos import ConfigError -from vyos.util import call -from vyos.template import render - -from vyos import airbag +from vyos import ConfigError, airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' @@ -61,7 +57,7 @@ default_config_data = { 'trap_targets': [], 'vyos_user': '', 'vyos_user_pass': '', - 'version': '999', + 'version': '', 'v3_enabled': 'False', 'v3_engineid': '', 'v3_groups': [], @@ -90,9 +86,8 @@ def get_config(): snmp['version'] = version_data['version'] # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx' - # os.urandom(8) returns 8 bytes of random data - snmp['vyos_user'] = 'vyos' + hexlify(os.urandom(8)).decode('utf-8') - snmp['vyos_user_pass'] = hexlify(os.urandom(16)).decode('utf-8') + snmp['vyos_user'] = 'vyos' + random(8) + snmp['vyos_user_pass'] = random(16) if conf.exists('community'): for name in conf.list_nodes('community'): @@ -263,30 +258,30 @@ def get_config(): # cmdline option '-a' trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap)) - if conf.exists('v3 trap-target {0} auth plaintext-key'.format(trap)): + if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)): # Set the authentication pass phrase used for authenticated SNMPv3 messages. # cmdline option '-A' - trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-key'.format(trap)) + trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap)) - if conf.exists('v3 trap-target {0} auth encrypted-key'.format(trap)): + if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)): # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys. # cmdline option '-3m' - trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-key'.format(trap)) + trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap)) if conf.exists('v3 trap-target {0} privacy type'.format(trap)): # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages. # cmdline option '-x' trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap)) - if conf.exists('v3 trap-target {0} privacy plaintext-key'.format(trap)): + if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)): # Set the privacy pass phrase used for encrypted SNMPv3 messages. # cmdline option '-X' - trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-key'.format(trap)) + trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap)) - if conf.exists('v3 trap-target {0} privacy encrypted-key'.format(trap)): + if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)): # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys. # cmdline option '-3M' - trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-key'.format(trap)) + trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap)) if conf.exists('v3 trap-target {0} protocol'.format(trap)): trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap)) @@ -325,11 +320,11 @@ def get_config(): } # v3 user {0} auth - if conf.exists('v3 user {0} auth encrypted-key'.format(user)): - user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user)) + if conf.exists('v3 user {0} auth encrypted-password'.format(user)): + user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user)) - if conf.exists('v3 user {0} auth plaintext-key'.format(user)): - user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-key'.format(user)) + if conf.exists('v3 user {0} auth plaintext-password'.format(user)): + user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user)) # load default value type = user_cfg['authProtocol'] @@ -349,11 +344,11 @@ def get_config(): user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user)) # v3 user {0} privacy - if conf.exists('v3 user {0} privacy encrypted-key'.format(user)): - user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user)) + if conf.exists('v3 user {0} privacy encrypted-password'.format(user)): + user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user)) - if conf.exists('v3 user {0} privacy plaintext-key'.format(user)): - user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user)) + if conf.exists('v3 user {0} privacy plaintext-password'.format(user)): + user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user)) # load default value type = user_cfg['privProtocol'] @@ -450,16 +445,16 @@ def verify(snmp): if 'v3_traps' in snmp.keys(): for trap in snmp['v3_traps']: if trap['authPassword'] and trap['authMasterKey']: - raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap auth') + raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth') if trap['authPassword'] == '' and trap['authMasterKey'] == '': - raise ConfigError('Must specify encrypted-key or plaintext-key for trap auth') + raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth') if trap['privPassword'] and trap['privMasterKey']: - raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap privacy') + raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy') if trap['privPassword'] == '' and trap['privMasterKey'] == '': - raise ConfigError('Must specify encrypted-key or plaintext-key for trap privacy') + raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy') if not 'type' in trap.keys(): raise ConfigError('v3 trap: "type" must be specified') @@ -490,19 +485,12 @@ def verify(snmp): if error: raise ConfigError('You must create group "{0}" first'.format(user['group'])) - # Depending on the configured security level - # the user has to provide additional info - if user['authPassword'] and user['authMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') - + # Depending on the configured security level the user has to provide additional info if (not user['authPassword'] and not user['authMasterKey']): - raise ConfigError('Must specify encrypted-key or plaintext-key for user auth') - - if user['privPassword'] and user['privMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') + raise ConfigError('Must specify encrypted-password or plaintext-key for user auth') if user['privPassword'] == '' and user['privMasterKey'] == '': - raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') + raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy') if user['mode'] == '': raise ConfigError('Must specify user mode ro/rw') @@ -524,12 +512,36 @@ def generate(snmp): for file in config_files: rmfile(file) - # Reload systemd manager configuration - call('systemctl daemon-reload') - if not snmp: return None + if 'v3_users' in snmp.keys(): + # net-snmp is now regenerating the configuration file in the background + # thus we need to re-open and re-read the file as the content changed. + # After that we can no read the encrypted password from the config and + # replace the CLI plaintext password with its encrypted version. + os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" + + for user in snmp['v3_users']: + if user['authProtocol'] == 'sha': + hash = plaintext_to_sha1 + else: + hash = plaintext_to_md5 + + if user['authPassword']: + user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid']) + user['authPassword'] = '' + + call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user)) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user)) + + if user['privPassword']: + user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid']) + user['privPassword'] = '' + + call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user)) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user)) + # Write client config file render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) # Write server config file @@ -544,50 +556,14 @@ def generate(snmp): return None def apply(snmp): + # Always reload systemd manager configuration + call('systemctl daemon-reload') + if not snmp: return None - # Reload systemd manager configuration - call('systemctl daemon-reload') # start SNMP daemon - call("systemctl restart snmpd.service") - - if 'vrf' not in snmp.keys(): - # service will be restarted multiple times later on - while (call('systemctl -q is-active snmpd.service') != 0): - sleep(0.5) - - # net-snmp is now regenerating the configuration file in the background - # thus we need to re-open and re-read the file as the content changed. - # After that we can no read the encrypted password from the config and - # replace the CLI plaintext password with its encrypted version. - os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" - - # XXX: actually this whole logic makes less sense - why not calculate the - # password hashed on our own and write them back into the config? I see - # no valid reason in waiting for a third party process to do so. - with open(config_file_user, 'r') as f: - engineID = '' - for line in f: - if line.startswith('usmUser'): - string = line.split(' ') - cfg = { - 'user': string[4].replace(r'"', ''), - 'auth_pw': string[8], - 'priv_pw': string[10] - } - # No need to take care about the VyOS internal user - if cfg['user'] == snmp['vyos_user']: - continue - - # Now update the running configuration - # - # Currently when executing call() the environment does not - # have the vyos_libexec_dir variable set, see Phabricator T685. - call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) - call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) - call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) - call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + call('systemctl restart snmpd.service') # Enable AgentX in FRR call('vtysh -c "configure terminal" -c "agentx" >/dev/null') diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index ffb0b700d..7b262565a 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -28,7 +28,7 @@ from vyos.xml import defaults from vyos import airbag airbag.enable() -config_file = r'/etc/ssh/sshd_config' +config_file = r'/run/ssh/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' def get_config(): @@ -42,6 +42,8 @@ def get_config(): # options which we need to update into the dictionary retrived. default_values = defaults(base) ssh = dict_merge(default_values, ssh) + # pass config file path - used in override template + ssh['config_file'] = config_file return ssh diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 93d4cc679..b1dd583b5 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -72,7 +72,7 @@ def get_config(): user = { 'name': username, 'password_plaintext': '', - 'password_encred': '!', + 'password_encrypted': '!', 'public_keys': [], 'full_name': '', 'home_dir': '/home/' + username, diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py index 8de3b6fa2..0aacd19d8 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-options.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -16,67 +16,83 @@ import os +from netifaces import interfaces from sys import exit -from copy import deepcopy + from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos.validate import is_addr_assigned from vyos import ConfigError -from vyos.util import run - from vyos import airbag airbag.enable() -systemd_ctrl_alt_del = '/lib/systemd/system/ctrl-alt-del.target' - -default_config_data = { - 'beep_if_fully_booted': False, - 'ctrl_alt_del': 'ignore', - 'reboot_on_panic': True -} +curlrc_config = r'/etc/curlrc' +ssh_config = r'/etc/ssh/ssh_config' +systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' def get_config(): - opt = deepcopy(default_config_data) conf = Config() - conf.set_level('system options') - if conf.exists(''): - if conf.exists('ctrl-alt-del-action'): - opt['ctrl_alt_del'] = conf.return_value('ctrl-alt-del-action') - - opt['beep_if_fully_booted'] = conf.exists('beep-if-fully-booted') - opt['reboot_on_panic'] = conf.exists('reboot-on-panic') - - return opt - -def verify(opt): - pass - -def generate(opt): - pass - -def apply(opt): + base = ['system', 'options'] + options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return options + +def verify(options): + if 'http_client' in options: + config = options['http_client'] + if 'source_interface' in config: + if not config['source_interface'] in interfaces(): + raise ConfigError(f'Source interface {source_interface} does not ' + f'exist'.format(**config)) + + if {'source_address', 'source_interface'} <= set(config): + raise ConfigError('Can not define both HTTP source-interface and source-address') + + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + + if 'ssh_client' in options: + config = options['ssh_client'] + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + + return None + +def generate(options): + render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True) + render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True) + return None + +def apply(options): # Beep action - if opt['beep_if_fully_booted']: - run('systemctl enable vyos-beep.service') + if 'beep_if_fully_booted' in options.keys(): + call('systemctl enable vyos-beep.service') else: - run('systemctl disable vyos-beep.service') + call('systemctl disable vyos-beep.service') # Ctrl-Alt-Delete action - if opt['ctrl_alt_del'] == 'ignore': - if os.path.exists(systemd_ctrl_alt_del): - os.unlink('/lib/systemd/system/ctrl-alt-del.target') + if os.path.exists(systemd_action_file): + os.unlink(systemd_action_file) + + if 'ctrl_alt_del_action' in options: + if options['ctrl_alt_del_action'] == 'reboot': + os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) + elif options['ctrl_alt_del_action'] == 'poweroff': + os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) - elif opt['ctrl_alt_del'] == 'reboot': - if os.path.exists(systemd_ctrl_alt_del): - os.unlink(systemd_ctrl_alt_del) - os.symlink('/lib/systemd/system/reboot.target', systemd_ctrl_alt_del) + if 'http_client' not in options: + if os.path.exists(curlrc_config): + os.unlink(curlrc_config) - elif opt['ctrl_alt_del'] == 'poweroff': - if os.path.exists(systemd_ctrl_alt_del): - os.unlink(systemd_ctrl_alt_del) - os.symlink('/lib/systemd/system/poweroff.target', systemd_ctrl_alt_del) + if 'ssh_client' not in options: + if os.path.exists(ssh_config): + os.unlink(ssh_config) # Reboot system on kernel panic with open('/proc/sys/kernel/panic', 'w') as f: - if opt['reboot_on_panic']: + if 'reboot_on_panic' in options.keys(): f.write('60') else: f.write('0') diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 88df2902e..26ad1af84 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -151,7 +151,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -162,6 +163,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 4536692d2..32cbadd74 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -111,7 +111,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -122,6 +123,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 4c4d8e403..ddb499bf4 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -118,7 +118,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -129,6 +130,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf index 44c4345e1..c2f640cf5 100644 --- a/src/etc/systemd/system/radvd.service.d/override.conf +++ b/src/etc/systemd/system/radvd.service.d/override.conf @@ -1,17 +1,17 @@ -[Unit]
-ConditionPathExists=/run/radvd/radvd.conf
-After=
-After=vyos-router.service
-
-[Service]
-WorkingDirectory=
-WorkingDirectory=/run/radvd
-ExecStartPre=
-ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
-ExecStart=
-ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid
-ExecReload=
-ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
-ExecReload=/bin/kill -HUP $MAINPID
-PIDFile=
-PIDFile=/run/radvd/radvd.pid
+[Unit] +ConditionPathExists=/run/radvd/radvd.conf +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/radvd +ExecStartPre= +ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecStart= +ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid +ExecReload= +ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecReload=/bin/kill -HUP $MAINPID +PIDFile= +PIDFile=/run/radvd/radvd.pid diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index a9fa15778..c2da1bb11 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -27,12 +27,12 @@ import sys import tempfile import vyos.defaults import vyos.remote -from vyos.config import Config, VyOSError +from vyos.configsource import ConfigSourceSession, VyOSError from vyos.migrator import Migrator, VirtualMigrator, MigratorError -class LoadConfig(Config): +class LoadConfig(ConfigSourceSession): """A subclass for calling 'loadFile'. - This does not belong in config.py, and only has a single caller. + This does not belong in configsource.py, and only has a single caller. """ def load_config(self, path): return self._run(['/bin/cli-shell-api','loadFile',path]) diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 index 8830ffdc7..a4051301f 100755 --- a/src/migration-scripts/interfaces/7-to-8 +++ b/src/migration-scripts/interfaces/7-to-8 @@ -17,8 +17,23 @@ # Split WireGuard endpoint into address / port nodes to make use of common # validators +import os + from sys import exit, argv from vyos.configtree import ConfigTree +from vyos.util import chown, chmod_750 + +def migrate_default_keys(): + kdir = r'/config/auth/wireguard' + if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): + location = f'{kdir}/default' + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) + os.rename(f'{kdir}/private.key', f'{location}/private.key') + os.rename(f'{kdir}/public.key', f'{location}/public.key') if __name__ == '__main__': if (len(argv) < 1): @@ -32,6 +47,8 @@ if __name__ == '__main__': config = ConfigTree(config_file) base = ['interfaces', 'wireguard'] + migrate_default_keys() + if not config.exists(base): # Nothing to do exit(0) diff --git a/src/migration-scripts/snmp/1-to-2 b/src/migration-scripts/snmp/1-to-2 new file mode 100755 index 000000000..466a624e6 --- /dev/null +++ b/src/migration-scripts/snmp/1-to-2 @@ -0,0 +1,89 @@ +#!/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/>. + +from sys import argv, exit +from vyos.configtree import ConfigTree + +def migrate_keys(config, path): + # authentication: rename node 'encrypted-key' -> 'encrypted-password' + config_path_auth = path + ['auth', 'encrypted-key'] + if config.exists(config_path_auth): + config.rename(config_path_auth, 'encrypted-password') + config_path_auth = path + ['auth', 'encrypted-password'] + + # remove leading '0x' from string if present + tmp = config.return_value(config_path_auth) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_path_auth, value=tmp) + + # privacy: rename node 'encrypted-key' -> 'encrypted-password' + config_path_priv = path + ['privacy', 'encrypted-key'] + if config.exists(config_path_priv): + config.rename(config_path_priv, 'encrypted-password') + config_path_priv = path + ['privacy', 'encrypted-password'] + + # remove leading '0x' from string if present + tmp = config.return_value(config_path_priv) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_path_priv, value=tmp) + +if __name__ == '__main__': + 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) + config_base = ['service', 'snmp', 'v3'] + + if not config.exists(config_base): + # Nothing to do + exit(0) + else: + # We no longer support hashed values prefixed with '0x' to unclutter + # CLI and also calculate the hases in advance instead of retrieving + # them after service startup - which was always a bad idea + prefix = '0x' + + config_engineid = config_base + ['engineid'] + if config.exists(config_engineid): + tmp = config.return_value(config_engineid) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_engineid, value=tmp) + + config_user = config_base + ['user'] + if config.exists(config_user): + for user in config.list_nodes(config_user): + migrate_keys(config, config_user + [user]) + + config_trap = config_base + ['trap-target'] + if config.exists(config_trap): + for trap in config.list_nodes(config_trap): + migrate_keys(config, config_trap + [trap]) + + 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/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2 new file mode 100755 index 000000000..bc8815753 --- /dev/null +++ b/src/migration-scripts/ssh/1-to-2 @@ -0,0 +1,55 @@ +#!/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/>. + +# VyOS 1.2 crux allowed configuring a lower or upper case loglevel. This +# is no longer supported as the input data is validated and will lead to +# an error. If user specifies an upper case logleve, make it lowercase + +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() + +base = ['service', 'ssh', 'loglevel'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) +else: + # red in configured loglevel and convert it to lower case + tmp = config.return_value(base).lower() + + # VyOS 1.2 had no proper value validation on the CLI thus the + # user could use any arbitrary values - sanitize them + if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']: + tmp = 'info' + + config.set(base, value=tmp) + + 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/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index bf8c39fd6..6586cbceb 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -21,58 +21,57 @@ import re import ipaddress import os.path from tabulate import tabulate - +from json import loads from vyos.util import cmd, run +from vyos.logger import syslog # some default values uacctd_pidfile = '/var/run/uacctd.pid' uacctd_pipefile = '/tmp/uacctd.pipe' - -# check if ports argument have correct format -def _is_ports(ports): - # define regex for checking - regex_filter = re.compile('^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$|^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])-(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$|^((\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]),)+(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if not regex_filter.search(ports): - raise argparse.ArgumentTypeError("Invalid ports: {}".format(ports)) - - # check which type nitation is used: single port, ports list, ports range - # single port - regex_filter = re.compile('^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if regex_filter.search(ports): - filter_ports = { 'type': 'single', 'value': int(ports) } - - # ports list - regex_filter = re.compile('^((\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]),)+(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])') - if regex_filter.search(ports): - filter_ports = { 'type': 'list', 'value': list(map(int, ports.split(','))) } - - # ports range - regex_filter = re.compile('^(?P<first>\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])-(?P<second>\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if regex_filter.search(ports): - # check if second number is greater than the first - if int(regex_filter.search(ports).group('first')) >= int(regex_filter.search(ports).group('second')): - raise argparse.ArgumentTypeError("Invalid ports: {}".format(ports)) - filter_ports = { 'type': 'range', 'value': range(int(regex_filter.search(ports).group('first')), int(regex_filter.search(ports).group('second'))) } - - # if all above failed - if not filter_ports: - raise argparse.ArgumentTypeError("Failed to parse: {}".format(ports)) +def parse_port(port): + try: + port_num = int(port) + if (port_num >= 0) and (port_num <= 65535): + return port_num + else: + raise ValueError("out of the 0-65535 range".format(port)) + except ValueError as e: + raise ValueError("Incorrect port number \'{0}\': {1}".format(port, e)) + +def parse_ports(arg): + if re.match(r'^\d+$', arg): + # Single port + port = parse_port(arg) + return {"type": "single", "value": port} + elif re.match(r'^\d+\-\d+$', arg): + # Port range + ports = arg.split("-") + ports = list(map(parse_port, ports)) + if ports[0] > ports[1]: + raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg)) + else: + return {"type": "range", "value": (ports[0], ports[1])} + elif re.match(r'^\d+,.*\d$', arg): + # Port list + ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4' + ports = list(map(parse_port, ports)) + return {"type": "list", "value": ports} else: - return filter_ports + raise ValueError("Malformed port spec \'{0}\'".format(arg)) # check if host argument have correct format -def _is_host(host): +def check_host(host): # define regex for checking if not ipaddress.ip_address(host): - raise argparse.ArgumentTypeError("Invalid host: {}".format(host)) - return host + raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host)) # check if flow-accounting running def _uacctd_running(): command = 'systemctl status uacctd.service > /dev/null' return run(command) == 0 + # get list of interfaces def _get_ifaces_dict(): # run command to get ifaces list @@ -83,7 +82,7 @@ def _get_ifaces_dict(): # make a dictionary with interfaces and indexes ifaces_dict = {} - regex_filter = re.compile('^(?P<iface_index>\d+):\ (?P<iface_name>[\w\d\.]+)[:@].*$') + regex_filter = re.compile(r'^(?P<iface_index>\d+):\ (?P<iface_name>[\w\d\.]+)[:@].*$') for iface_line in ifaces_out: if regex_filter.search(iface_line): ifaces_dict[int(regex_filter.search(iface_line).group('iface_index'))] = regex_filter.search(iface_line).group('iface_name') @@ -91,11 +90,12 @@ def _get_ifaces_dict(): # return dictioanry return ifaces_dict + # get list of flows def _get_flows_list(): # run command to get flows list out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}', - message='Failed to get flows list') + message='Failed to get flows list') # read output flows_out = out.splitlines() @@ -103,11 +103,15 @@ def _get_flows_list(): # make a list with flows flows_list = [] for flow_line in flows_out: - flows_list.append(eval(flow_line)) + try: + flows_list.append(loads(flow_line)) + except Exception as err: + syslog.error('Unable to read flow info: {}'.format(err)) # return list of flows return flows_list + # filter and format flows def _flows_filter(flows, ifaces): # predefine filtered flows list @@ -149,14 +153,29 @@ def _flows_filter(flows, ifaces): # return filtered flows return flows_filtered + # print flow table def _flows_table_print(flows): - #define headers and body - table_headers = [ 'IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES' ] + # define headers and body + table_headers = ['IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES'] table_body = [] # convert flows to list for flow in flows: - table_body.append([flow['iface_in_name'], flow['mac_src'], flow['mac_dst'], flow['ip_src'], flow['ip_dst'], flow['port_src'], flow['port_dst'], flow['ip_proto'], flow['tos'], flow['packets'], flow['flows'], flow['bytes'] ]) + table_line = [ + flow.get('iface_in_name'), + flow.get('mac_src'), + flow.get('mac_dst'), + flow.get('ip_src'), + flow.get('ip_dst'), + flow.get('port_src'), + flow.get('port_dst'), + flow.get('ip_proto'), + flow.get('tos'), + flow.get('packets'), + flow.get('flows'), + flow.get('bytes') + ] + table_body.append(table_line) # configure and fill table table = tabulate(table_body, table_headers, tablefmt="simple") @@ -168,23 +187,34 @@ def _flows_table_print(flows): except KeyboardInterrupt: sys.exit(0) + # check if in-memory table is active def _check_imt(): if not os.path.exists(uacctd_pipefile): print("In-memory table is not available") sys.exit(1) + # define program arguments cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting') cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon') cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display') cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration') -cmd_args_parser.add_argument('--host', type=_is_host, required=False, help='host address for output filtration') -cmd_args_parser.add_argument('--ports', type=_is_ports, required=False, help='ports number for output filtration') -cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtration') +cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering') +cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering') +cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering') # parse arguments cmd_args = cmd_args_parser.parse_args() +try: + if cmd_args.host: + check_host(cmd_args.host) + + if cmd_args.ports: + cmd_args.ports = parse_ports(cmd_args.ports) +except ValueError as e: + print(e) + sys.exit(1) # main logic # do nothing if uacctd daemon is not running diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index 5d48e3210..0df6749aa 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -14,19 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - import argparse import jinja2 -from xml.dom import minidom from sys import exit from tabulate import tabulate +from xml.dom import minidom -from vyos.util import popen +from vyos.util import cmd from vyos.config import Config parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") +parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") # Please be careful if you edit the template. @@ -40,10 +40,8 @@ Device ID Local Proto Cap Platform Port ID {% endfor -%} """ -def _get_neighbors(): - command = '/usr/sbin/lldpcli -f xml show neighbors' - out,_ = popen(command) - return out +def get_neighbors(): + return cmd('/usr/sbin/lldpcli -f xml show neighbors') def extract_neighbor(neighbor): """ @@ -148,12 +146,17 @@ if __name__ == '__main__': exit(0) if args.all: - neighbors = minidom.parseString(_get_neighbors()) + neighbors = minidom.parseString(get_neighbors()) for neighbor in neighbors.getElementsByTagName('interface'): tmp['neighbors'].append( extract_neighbor(neighbor) ) + elif args.detail: + out = cmd('/usr/sbin/lldpctl -f plain') + print(out) + exit(0) + elif args.interface: - neighbors = minidom.parseString(_get_neighbors()) + neighbors = minidom.parseString(get_neighbors()) for neighbor in neighbors.getElementsByTagName('interface'): # check if neighbor appeared on proper interface if neighbor.getAttribute('name') == args.interface: diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index f9577e57e..ff1e3cc56 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -161,7 +161,8 @@ def get_pool_size(config, pool): start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - size += int(ip_address(stop)) - int(ip_address(start)) + # Add +1 because both range boundaries are inclusive + size += int(ip_address(stop)) - int(ip_address(start)) + 1 return size diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 46571c0c0..d4dae3cd1 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -220,8 +220,7 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp): oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ] admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] - # do not ask me why 56, it was the number in the perl code ... - descs = list(split_text(interface.get_alias(),56)) + descs = list(split_text(interface.get_alias(),0)) while intf or oper or admin or addrs or descs: i = intf.pop(0) if intf else '' diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 15bf63e81..e08bc983a 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -21,22 +21,17 @@ import shutil import syslog as sl import re +from vyos.config import Config from vyos.ifconfig import WireGuardIf - +from vyos.util import cmd +from vyos.util import run +from vyos.util import check_kmod from vyos import ConfigError -from vyos.config import Config -from vyos.util import cmd, run dir = r'/config/auth/wireguard' psk = dir + '/preshared.key' -def check_kmod(): - """ check if kmod is loaded, if not load it """ - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if run('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") +k_mod = 'wireguard' def generate_keypair(pk, pub): """ generates a keypair which is stored in /config/auth/wireguard """ @@ -106,7 +101,7 @@ def del_key_dir(kname): if __name__ == '__main__': - check_kmod() + check_kmod(k_mod) parser = argparse.ArgumentParser(description='wireguard key management') parser.add_argument( '--genkey', action="store_true", help='generate key-pair') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4c41fa96d..d5730d86c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,7 +32,6 @@ from waitress import serve from functools import wraps from vyos.configsession import ConfigSession, ConfigSessionError -from vyos.config import VyOSError DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' @@ -231,8 +230,6 @@ def retrieve_op(command): return error(400, "\"{0}\" is not a valid config format".format(config_format)) else: return error(400, "\"{0}\" is not a valid operation".format(op)) - except VyOSError as e: - return error(400, str(e)) except ConfigSessionError as e: return error(400, str(e)) except Exception as e: diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service index 21e189e4b..7e0bee8e1 100644 --- a/src/systemd/wpa_supplicant-macsec@.service +++ b/src/systemd/wpa_supplicant-macsec@.service @@ -1,17 +1,17 @@ -[Unit]
-Description=WPA supplicant daemon (macsec-specific version)
-Requires=sys-subsystem-net-devices-%i.device
-ConditionPathExists=/run/wpa_supplicant/%I.conf
-After=vyos-router.service
-RequiresMountsFor=/run
-
-# NetworkManager users will probably want the dbus version instead.
-
-[Service]
-Type=simple
-WorkingDirectory=/run/wpa_supplicant
-PIDFile=/run/wpa_supplicant/%I.pid
-ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
-
-[Install]
-WantedBy=multi-user.target
+[Unit] +Description=WPA supplicant daemon (macsec-specific version) +Requires=sys-subsystem-net-devices-%i.device +ConditionPathExists=/run/wpa_supplicant/%I.conf +After=vyos-router.service +RequiresMountsFor=/run + +# NetworkManager users will probably want the dbus version instead. + +[Service] +Type=simple +WorkingDirectory=/run/wpa_supplicant +PIDFile=/run/wpa_supplicant/%I.pid +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py index c4c59b827..1597025e8 100644 --- a/src/tests/test_initial_setup.py +++ b/src/tests/test_initial_setup.py @@ -21,6 +21,7 @@ import tempfile import unittest from unittest import TestCase, mock +from vyos import xml import vyos.configtree import vyos.initialsetup as vis @@ -30,6 +31,7 @@ class TestInitialSetup(TestCase): with open('tests/data/config.boot.default', 'r') as f: config_string = f.read() self.config = vyos.configtree.ConfigTree(config_string) + self.xml = xml.load_configuration() def test_set_user_password(self): vis.set_user_password(self.config, 'vyos', 'vyosvyos') @@ -56,7 +58,7 @@ class TestInitialSetup(TestCase): self.assertEqual(key_type, 'ssh-rsa') self.assertEqual(key_data, 'fakedata') - self.assertTrue(self.config.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) def test_set_ssh_key_without_name(self): # If key file doesn't include a name, the function will use user name for the key name @@ -69,7 +71,7 @@ class TestInitialSetup(TestCase): self.assertEqual(key_type, 'ssh-rsa') self.assertEqual(key_data, 'fakedata') - self.assertTrue(self.config.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) def test_create_user(self): vis.create_user(self.config, 'jrandomhacker', password='qwerty', key=" ssh-rsa fakedata jrandomhacker@foovax ") @@ -95,8 +97,8 @@ class TestInitialSetup(TestCase): vis.set_default_gateway(self.config, '192.0.2.1') self.assertTrue(self.config.exists(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '192.0.2.1'])) - self.assertTrue(self.config.is_tag(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop'])) - self.assertTrue(self.config.is_tag(['protocols', 'static', 'route'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route'])) if __name__ == "__main__": unittest.main() diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal new file mode 100755 index 000000000..652110346 --- /dev/null +++ b/src/validators/dotted-decimal @@ -0,0 +1,33 @@ +#!/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/>. + +import re +import sys + +area = sys.argv[1] + +res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area) +if not res: + print("\'{0}\' is not a valid dotted decimal value".format(area)) + sys.exit(1) +else: + components = res.groups() + for n in range(0, 4): + if (int(components[n]) > 255): + print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n])) + sys.exit(1) + +sys.exit(0) |