diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/https.py | 9 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-pppoe.py | 6 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 35 | ||||
| -rw-r--r-- | src/etc/sysctl.d/30-vyos-router.conf | 8 | ||||
| -rwxr-xr-x | src/helpers/vyos-load-config.py | 2 | ||||
| -rwxr-xr-x | src/migration-scripts/https/4-to-5 | 62 | ||||
| -rwxr-xr-x | src/migration-scripts/interfaces/31-to-32 | 4 | ||||
| -rwxr-xr-x | src/op_mode/bridge.py | 29 | ||||
| -rwxr-xr-x | src/op_mode/interfaces.py | 7 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 76 | 
10 files changed, 214 insertions, 24 deletions
| diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 010490c7e..26c4343a0 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -122,7 +122,7 @@ def verify(https):              server_block = deepcopy(default_server_block)              data = vhost_dict.get(vhost, {})              server_block['address'] = data.get('listen-address', '*') -            server_block['port'] = data.get('listen-port', '443') +            server_block['port'] = data.get('port', '443')              server_block_list.append(server_block)      for entry in server_block_list: @@ -156,7 +156,7 @@ def generate(https):              server_block['id'] = vhost              data = vhost_dict.get(vhost, {})              server_block['address'] = data.get('listen-address', '*') -            server_block['port'] = data.get('listen-port', '443') +            server_block['port'] = data.get('port', '443')              name = data.get('server-name', ['_'])              server_block['name'] = name              allow_client = data.get('allow-client', {}) @@ -215,14 +215,9 @@ def generate(https):          api_data = vyos.defaults.api_data      api_settings = https.get('api', {})      if api_settings: -        port = api_settings.get('port', '') -        if port: -            api_data['port'] = port          vhosts = https.get('api-restrict', {}).get('virtual-host', [])          if vhosts:              api_data['vhost'] = vhosts[:] -        if 'socket' in list(api_settings): -            api_data['socket'] = True      if api_data:          vhost_list = api_data.get('vhost', []) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 0a03a172c..42f084309 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -61,6 +61,12 @@ def get_config(config=None):              # bail out early - no need to further process other nodes              break +    if 'deleted' not in pppoe: +        # We always set the MRU value to the MTU size. This code path only re-creates +        # the old behavior if MRU is not set on the CLI. +        if 'mru' not in pppoe: +            pppoe['mru'] = pppoe['mtu'] +      return pppoe  def verify(pppoe): diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 6bf3227d5..4251e611b 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -60,8 +60,14 @@ def get_config(config=None):              vxlan.update({'rebuild_required': {}})              break +    # When dealing with VNI filtering we need to know what VNI was actually removed, +    # so build up a dict matching the vlan_to_vni structure but with removed values.      tmp = node_changed(conf, base + [ifname, 'vlan-to-vni'], recursive=True) -    if tmp: vxlan.update({'vlan_to_vni_removed': tmp}) +    if tmp: +        vxlan.update({'vlan_to_vni_removed': {}}) +        for vlan in tmp: +            vni = leaf_node_changed(conf, base + [ifname, 'vlan-to-vni', vlan, 'vni']) +            vxlan['vlan_to_vni_removed'].update({vlan : {'vni' : vni[0]}})      # We need to verify that no other VXLAN tunnel is configured when external      # mode is in use - Linux Kernel limitation @@ -98,14 +104,31 @@ def verify(vxlan):      if 'vni' not in vxlan and dict_search('parameters.external', vxlan) == None:          raise ConfigError('Must either configure VXLAN "vni" or use "external" CLI option!') -    if dict_search('parameters.external', vxlan): +    if dict_search('parameters.external', vxlan) != None:          if 'vni' in vxlan:              raise ConfigError('Can not specify both "external" and "VNI"!')          if 'other_tunnels' in vxlan: -            other_tunnels = ', '.join(vxlan['other_tunnels']) -            raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\ -                              f'CLI option is used. Additional tunnels: {other_tunnels}') +            # When multiple VXLAN interfaces are defined and "external" is used, +            # all VXLAN interfaces need to have vni-filter enabled! +            # See Linux Kernel commit f9c4bb0b245cee35ef66f75bf409c9573d934cf9 +            other_vni_filter = False +            for tunnel, tunnel_config in vxlan['other_tunnels'].items(): +                if dict_search('parameters.vni_filter', tunnel_config) != None: +                    other_vni_filter = True +                    break +            # eqivalent of the C foo ? 'a' : 'b' statement +            vni_filter = True and (dict_search('parameters.vni_filter', vxlan) != None) or False +            # If either one is enabled, so must be the other. Both can be off and both can be on +            if (vni_filter and not other_vni_filter) or (not vni_filter and other_vni_filter): +                raise ConfigError(f'Using multiple VXLAN interfaces with "external" '\ +                    'requires all VXLAN interfaces to have "vni-filter" configured!') + +            if not vni_filter and not other_vni_filter: +                other_tunnels = ', '.join(vxlan['other_tunnels']) +                raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\ +                                f'CLI option is used and "vni-filter" is unset. '\ +                                f'Additional tunnels: {other_tunnels}')      if 'gpe' in vxlan and 'external' not in vxlan:          raise ConfigError(f'VXLAN-GPE is only supported when "external" '\ @@ -165,7 +188,7 @@ def verify(vxlan):                  raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')              vnis_used.append(vni) -    if dict_search('parameters.neighbor_suppress', vxlan): +    if dict_search('parameters.neighbor_suppress', vxlan) != None:          if 'is_bridge_member' not in vxlan:              raise ConfigError('Neighbor suppression requires that VXLAN interface '\                                'is member of a bridge interface!') diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 1c9b8999f..67d96969e 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -105,3 +105,11 @@ net.core.rps_sock_flow_entries = 32768  net.core.default_qdisc=fq_codel  net.ipv4.tcp_congestion_control=bbr +# VRF - Virtual routing and forwarding +# When net.vrf.strict_mode=0 (default) it is possible to associate multiple +# VRF devices to the same table. Conversely, when net.vrf.strict_mode=1 a +# table can be associated to a single VRF device. +# +# A VRF table can be used by the VyOS CLI only once (ensured by verify()), +# this simply adds an additional Kernel safety net +net.vrf.strict_mode=1 diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index e579e81b2..4ec865454 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -66,7 +66,7 @@ def get_local_config(filename):      return config_str -if any(x in file_name for x in protocols): +if any(file_name.startswith(f'{x}://') for x in protocols):      config_string = vyos.remote.get_remote_config(file_name)      if not config_string:          sys.exit(f"No such config file at '{file_name}'") diff --git a/src/migration-scripts/https/4-to-5 b/src/migration-scripts/https/4-to-5 new file mode 100755 index 000000000..0dfb6ac19 --- /dev/null +++ b/src/migration-scripts/https/4-to-5 @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# T5762: http: api: smoketests fail as they can not establish IPv6 connection +#        to uvicorn backend server, always make the UNIX domain socket the +#        default way of communication + +import sys + +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'https'] +if not config.exists(base): +    # Nothing to do +    sys.exit(0) + +# Delete "socket" CLI option - we always use UNIX domain sockets for +# NGINX <-> API server communication +if config.exists(base + ['api', 'socket']): +    config.delete(base + ['api', 'socket']) + +# There is no need for an API service port, as UNIX domain sockets +# are used +if config.exists(base + ['api', 'port']): +    config.delete(base + ['api', 'port']) + +# rename listen-port -> port ver virtual-host +if config.exists(base + ['virtual-host']): +    for vhost in config.list_nodes(base + ['virtual-host']): +        if config.exists(base + ['virtual-host', vhost, 'listen-port']): +            config.rename(base + ['virtual-host', vhost, 'listen-port'], 'port') + +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)) +    sys.exit(1) diff --git a/src/migration-scripts/interfaces/31-to-32 b/src/migration-scripts/interfaces/31-to-32 index ca3d19320..0fc27b70a 100755 --- a/src/migration-scripts/interfaces/31-to-32 +++ b/src/migration-scripts/interfaces/31-to-32 @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  #  # T5671: change port to IANA assigned default port +# T5759: change default MTU 1450 -> 1500  from sys import argv  from sys import exit @@ -43,6 +44,9 @@ for vxlan in config.list_nodes(base):      if not config.exists(base + [vxlan, 'port']):          config.set(base + [vxlan, 'port'], value='8472') +    if not config.exists(base + [vxlan, 'mtu']): +        config.set(base + [vxlan, 'mtu'], value='1450') +  try:      with open(file_name, 'w') as f:          f.write(config.to_string()) diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py index 185db4f20..412a4eba8 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -56,6 +56,13 @@ def _get_raw_data_vlan(tunnel:bool=False):      data_dict = json.loads(json_data)      return data_dict +def _get_raw_data_vni() -> dict: +    """ +    :returns dict +    """ +    json_data = cmd(f'bridge --json vni show') +    data_dict = json.loads(json_data) +    return data_dict  def _get_raw_data_fdb(bridge):      """Get MAC-address for the bridge brX @@ -165,6 +172,22 @@ def _get_formatted_output_vlan_tunnel(data):      output = tabulate(data_entries, headers)      return output +def _get_formatted_output_vni(data): +    data_entries = [] +    for entry in data: +        interface = entry.get('ifname') +        vlans = entry.get('vnis') +        for vlan_entry in vlans: +            vlan = vlan_entry.get('vni') +            if vlan_entry.get('vniEnd'): +                vlan_end = vlan_entry.get('vniEnd') +                vlan = f'{vlan}-{vlan_end}' +            data_entries.append([interface, vlan]) + +    headers = ["Interface", "VNI"] +    output = tabulate(data_entries, headers) +    return output +  def _get_formatted_output_fdb(data):      data_entries = []      for entry in data: @@ -228,6 +251,12 @@ def show_vlan(raw: bool, tunnel: typing.Optional[bool]):          else:              return _get_formatted_output_vlan(bridge_vlan) +def show_vni(raw: bool): +    bridge_vni = _get_raw_data_vni() +    if raw: +        return bridge_vni +    else: +        return _get_formatted_output_vni(bridge_vni)  def show_fdb(raw: bool, interface: str):      fdb_data = _get_raw_data_fdb(interface) diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index c626535b5..14ffdca9f 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -235,6 +235,11 @@ def _get_summary_data(ifname: typing.Optional[str],      if iftype is None:          iftype = ''      ret = [] + +    def is_interface_has_mac(interface_name): +        interface_no_mac = ('tun', 'wg') +        return not any(interface_name.startswith(prefix) for prefix in interface_no_mac) +      for interface in filtered_interfaces(ifname, iftype, vif, vrrp):          res_intf = {} @@ -244,7 +249,7 @@ def _get_summary_data(ifname: typing.Optional[str],          res_intf['addr'] = [_ for _ in interface.get_addr() if not _.startswith('fe80::')]          res_intf['description'] = interface.get_alias()          res_intf['mtu'] = interface.get_mtu() -        res_intf['mac'] = interface.get_mac() +        res_intf['mac'] = interface.get_mac() if is_interface_has_mac(interface.ifname) else 'n/a'          res_intf['vrf'] = interface.get_vrf()          ret.append(res_intf) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 3a9efb73e..85d7884b6 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -223,6 +223,19 @@ class ShowModel(ApiModel):              }          } +class RebootModel(ApiModel): +    op: StrictStr +    path: List[StrictStr] + +    class Config: +        schema_extra = { +            "example": { +                "key": "id_key", +                "op": "reboot", +                "path": ["op", "mode", "path"], +            } +        } +  class ResetModel(ApiModel):      op: StrictStr      path: List[StrictStr] @@ -236,6 +249,19 @@ class ResetModel(ApiModel):              }          } +class PoweroffModel(ApiModel): +    op: StrictStr +    path: List[StrictStr] + +    class Config: +        schema_extra = { +            "example": { +                "key": "id_key", +                "op": "poweroff", +                "path": ["op", "mode", "path"], +            } +        } +  class Success(BaseModel):      success: bool @@ -713,6 +739,26 @@ def show_op(data: ShowModel):      return success(res) +@app.post('/reboot') +def reboot_op(data: RebootModel): +    session = app.state.vyos_session + +    op = data.op +    path = data.path + +    try: +        if op == 'reboot': +            res = session.reboot(path) +        else: +            return error(400, f"'{op}' is not a valid operation") +    except ConfigSessionError as e: +        return error(400, str(e)) +    except Exception as e: +        logger.critical(traceback.format_exc()) +        return error(500, "An internal error occured. Check the logs for details.") + +    return success(res) +  @app.post('/reset')  def reset_op(data: ResetModel):      session = app.state.vyos_session @@ -733,6 +779,26 @@ def reset_op(data: ResetModel):      return success(res) +@app.post('/poweroff') +def poweroff_op(data: PoweroffModel): +    session = app.state.vyos_session + +    op = data.op +    path = data.path + +    try: +        if op == 'poweroff': +            res = session.poweroff(path) +        else: +            return error(400, f"'{op}' is not a valid operation") +    except ConfigSessionError as e: +        return error(400, str(e)) +    except Exception as e: +        logger.critical(traceback.format_exc()) +        return error(500, "An internal error occured. Check the logs for details.") + +    return success(res) +  ###  # GraphQL integration @@ -825,15 +891,7 @@ def initialization(session: ConfigSession, app: FastAPI = app):      if app.state.vyos_graphql:          graphql_init(app) -    if not server_config['socket']: -        config = ApiServerConfig(app, -                                 host=server_config["listen_address"], -                                 port=int(server_config["port"]), -                                 proxy_headers=True) -    else: -        config = ApiServerConfig(app, -                                 uds="/run/api.sock", -                                 proxy_headers=True) +    config = ApiServerConfig(app, uds="/run/api.sock", proxy_headers=True)      server = ApiServer(config)  def run_server(): | 
