diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/https.py | 10 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 19 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 3 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-dummy.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-geneve.py | 13 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-l2tpv3.py | 23 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 10 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-pseudo-ethernet.py | 8 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 148 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 12 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-wireless.py | 10 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-wirelessmodem.py | 9 | ||||
| -rwxr-xr-x | src/migration-scripts/https/1-to-2 | 54 | 
14 files changed, 313 insertions, 28 deletions
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index e46f1a4e7..777792229 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -88,14 +88,16 @@ def get_config():                      # certbot organizes certificates by first domain                      sb['certbot_dir'] = certbot_domains[0] +    api_somewhere = False      api_data = {}      if conf.exists('api'): +        api_somewhere = True          api_data = vyos.defaults.api_data          if conf.exists('api port'):              port = conf.return_value('api port')              api_data['port'] = port -        if conf.exists('api virtual-host'): -            vhosts = conf.return_values('api virtual-host') +        if conf.exists('api-restrict virtual-host'): +            vhosts = conf.return_values('api-restrict virtual-host')              api_data['vhost'] = vhosts[:]      if api_data: @@ -110,7 +112,9 @@ def get_config():                  if block['id'] in vhost_list:                      block['api'] = api_data -    https = {'server_block_list' : server_block_list, 'certbot': certbot} +    https = {'server_block_list' : server_block_list, +             'api_somewhere': api_somewhere, +             'certbot': certbot}      return https  def verify(https): diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 19f43f725..6a002bc06 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -24,9 +24,8 @@ from vyos.ifconfig import BondIf  from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config  from vyos.configdict import list_diff, vlan_to_dict  from vyos.config import Config +from vyos.util import run, is_bridge_member  from vyos import ConfigError -from vyos.util import run -  default_config_data = {      'address': [], @@ -278,17 +277,23 @@ def get_config():  def verify(bond): +    if bond['deleted']: +        interface = bond['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge)) +        return None +      if len (bond['arp_mon_tgt']) > 16:          raise ConfigError('The maximum number of targets that can be specified is 16')      if bond['primary']:          if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:              raise ConfigError('Mode dependency failed, primary not supported ' \ -                              'in this mode.'.format()) - -        if bond['primary'] not in bond['member']: -            raise ConfigError('Interface "{}" is not part of the bond' \ -                              .format(bond['primary'])) +                              'in mode "{}"!'.format(bond['mode']))      vrf_name = bond['vrf']      if vrf_name and vrf_name not in interfaces(): diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 28e5957e4..79247ee51 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -243,6 +243,9 @@ def verify(bridge):          if intf['name'] not in interfaces():              raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) +        if intf['name'] == 'lo': +            raise ConfigError('Loopback interface "lo" can not be added to a bridge') +      # bridge members are not allowed to be bond members, too      for intf in bridge['member']:          for bond in conf.list_nodes('interfaces bonding'): diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index b7b75517d..a256103af 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -23,6 +23,7 @@ from netifaces import interfaces  from vyos.ifconfig import DummyIf  from vyos.configdict import list_diff  from vyos.config import Config +from vyos.util import is_bridge_member  from vyos import ConfigError  default_config_data = { @@ -78,6 +79,16 @@ def get_config():      return dummy  def verify(dummy): +    if dummy['deleted']: +        interface = dummy['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge)) +        return None +      vrf_name = dummy['vrf']      if vrf_name and vrf_name not in interfaces():          raise ConfigError(f'VRF "{vrf_name}" does not exist') diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index eaa678d3e..e47473d76 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -18,11 +18,12 @@ import os  from sys import exit  from copy import deepcopy +from netifaces import interfaces  from vyos.config import Config -from vyos.ifconfig import GeneveIf, Interface +from vyos.ifconfig import GeneveIf +from vyos.util import is_bridge_member  from vyos import ConfigError -from netifaces import interfaces  default_config_data = {      'address': [], @@ -92,7 +93,13 @@ def get_config():  def verify(geneve):      if geneve['deleted']: -        # bail out early +        interface = geneve['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None      if not geneve['remote']: diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 4b5fc8306..0400cb849 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -22,7 +22,7 @@ from copy import deepcopy  from vyos.config import Config  from vyos.ifconfig import L2TPv3If, Interface  from vyos import ConfigError -from vyos.util import run +from vyos.util import run, is_bridge_member  from netifaces import interfaces  default_config_data = { @@ -154,27 +154,34 @@ def get_config():  def verify(l2tpv3): +    interface = l2tpv3['intf'] +      if l2tpv3['deleted']: -        # bail out early +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None      if not l2tpv3['local_address']: -        raise ConfigError('Must configure the l2tpv3 local-ip for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')      if not l2tpv3['remote_address']: -        raise ConfigError('Must configure the l2tpv3 remote-ip for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')      if not l2tpv3['tunnel_id']: -        raise ConfigError('Must configure the l2tpv3 tunnel-id for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}')      if not l2tpv3['peer_tunnel_id']: -        raise ConfigError('Must configure the l2tpv3 peer-tunnel-id for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}')      if not l2tpv3['session_id']: -        raise ConfigError('Must configure the l2tpv3 session-id for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}')      if not l2tpv3['peer_session_id']: -        raise ConfigError('Must configure the l2tpv3 peer-session-id for {}'.format(l2tpv3['intf'])) +        raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')      return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 1fe1143cd..e9b40bb38 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -31,7 +31,7 @@ from shutil import rmtree  from vyos.config import Config  from vyos.defaults import directories as vyos_data_dir  from vyos.ifconfig import VTunIf -from vyos.util import process_running, cmd +from vyos.util import process_running, cmd, is_bridge_member  from vyos.validate import is_addr_assigned  from vyos import ConfigError @@ -444,8 +444,16 @@ def get_config():  def verify(openvpn):      if openvpn['deleted']: +        interface = openvpn['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None +      if not openvpn['mode']:          raise ConfigError('Must specify OpenVPN operation mode') diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 56d4fdfc3..50b5a12a0 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -24,6 +24,7 @@ from vyos.ifconfig import MACVLANIf  from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config  from vyos.configdict import list_diff, vlan_to_dict  from vyos.config import Config +from vyos.util import is_bridge_member  from vyos import ConfigError  default_config_data = { @@ -217,6 +218,13 @@ def get_config():  def verify(peth):      if peth['deleted']: +        interface = peth['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None      if not peth['link']: diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 15863adc2..28b1cf60f 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -29,18 +29,99 @@ from vyos import ConfigError  class FixedDict(dict): +    """ +    FixedDict: A dictionnary not allowing new keys to be created after initialisation. + +    >>> f = FixedDict(**{'count':1}) +    >>> f['count'] = 2 +    >>> f['king'] = 3 +      File "...", line ..., in __setitem__ +    raise ConfigError(f'Option "{k}" has no defined default') +    """      def __init__ (self, **options):          self._allowed = options.keys()          super().__init__(**options)      def __setitem__ (self, k, v): +        """ +        __setitem__ is a builtin which is called by python when setting dict values: +        >>> d = dict() +        >>> d['key'] = 'value' +        >>> d +        {'key': 'value'} + +        is syntaxic sugar for + +        >>> d = dict() +        >>> d.__setitem__('key','value') +        >>> d +        {'key': 'value'} +        """          if k not in self._allowed:              raise ConfigError(f'Option "{k}" has no defined default')          super().__setitem__(k, v) -class ConfigurationState (Config): +class ConfigurationState(Config): +    """ +    The current API require a dict to be generated by get_config() +    which is then consumed by verify(), generate() and apply() + +    ConfiguartionState is an helper class wrapping Config and providing +    an common API to this dictionary structure + +    Its to_dict() function return a dictionary containing three fields, +    each a dict, called options, changes, actions. + +    options: + +    contains the configuration options for the dict and its value +    {'options': {'commment': 'test'}} will be set if +    'set interface dummy dum1 description test' was used and +    the key 'commment' is used to index the description info. + +    changes: + +    per key, let us know how the data was modified using one of the action +    a special key called 'section' is used to indicate what happened to the +    section. for example: + +    'set interface dummy dum1 description test' when no interface was setup +    will result in the following changes +    {'changes': {'section': 'create', 'comment': 'create'}} + +    on an existing interface, depending if there was a description +    'set interface dummy dum1 description test' will result in one of +    {'changes': {'comment': 'create'}}  (not present before) +    {'changes': {'comment': 'static'}}  (unchanged) +    {'changes': {'comment': 'modify'}}  (changed from half) + +    and 'delete interface dummy dummy1 description' will result in: +    {'changes': {'comment': 'delete'}} + +    actions: + +    for each action list the configuration key which were changes +    in our example if we added the 'description' and added an IP we would have +    {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + +    the actions are: +        'create': it did not exist previously and was created +        'modify': it did exist previously but its content changed +        'static': it did exist and did not change +        'delete': it was present but was removed from the configuration +        'absent': it was not and is not present +    which for each field represent how it was modified since the last commit +    """ +      def __init__ (self, section, default): +        """ +        initialise the class for a given configuration path: + +        >>> conf = ConfigurationState('interfaces ethernet eth1') +        all further references to get_value(s) and get_effective(s) +        will be for this part of the configuration (eth1) +        """          super().__init__()          self.section = section          self.default = deepcopy(default) @@ -61,6 +142,15 @@ class ConfigurationState (Config):              self.changes['section'] = 'create'      def _act(self, section): +        """ +        Returns for a given configuration field determine what happened to it + +        'create': it did not exist previously and was created +        'modify': it did exist previously but its content changed +        'static': it did exist and did not change +        'delete': it was present but was removed from the configuration +        'absent': it was not and is not present +        """          if self.exists(section):              if self.exists_effective(section):                  if self.return_value(section) != self.return_effective_value(section): @@ -89,24 +179,71 @@ class ConfigurationState (Config):          self.options[name] = value      def get_value(self, name, key, default=None): +        """ +        >>> conf.get_value('comment', 'description') +        will place the string of 'interface dummy description test' +        into the dictionnary entry 'comment' using Config.return_value +        (the data in the configuration to apply) +        """          if self._action(name, key) in ('delete', 'absent'):              return          return self._get(name, key, default, self.return_value)      def get_values(self, name, key, default=None): +        """ +        >>> conf.get_values('addresses-add', 'address') +        will place a list made of the IP present in 'interface dummy dum1 address' +        into the dictionnary entry 'addr' using Config.return_values +        (the data in the configuration to apply) +        """          if self._action(name, key) in ('delete', 'absent'):              return          return self._get(name, key, default, self.return_values)      def get_effective(self, name, key, default=None): +        """ +        >>> conf.get_value('comment', 'description') +        will place the string of 'interface dummy description test' +        into the dictionnary entry 'comment' using Config.return_effective_value +        (the data in the configuration to apply) +        """          self._action(name, key)          return self._get(name, key, default, self.return_effective_value)      def get_effectives(self, name, key, default=None): +        """ +        >>> conf.get_effectives('addresses-add', 'address') +        will place a list made of the IP present in 'interface ethernet eth1 address' +        into the dictionnary entry 'addresses-add' using Config.return_effectives_value +        (the data in the un-modified configuration) +        """          self._action(name, key)          return self._get(name, key, default, self.return_effectives_value)      def load(self, mapping): +        """ +        load will take a dictionary defining how we wish the configuration +        to be parsed and apply this definition to set the data. + +        >>> mapping = { +            'addresses-add' : ('address', True, None), +            'comment' : ('description', False, 'auto'), +        } +        >>> conf.load(mapping) + +        mapping is a dictionary where each key represents the name we wish +        to have (such as 'addresses-add'), with a list a content representing +        how the data should be parsed: +            - the configuration section name +              such as 'address' under 'interface ethernet eth1' +            - boolean indicating if this data can have multiple values +              for 'address', True, as multiple IPs can be set +              for 'description', False, as it is a single string +            - default represent the default value if absent from the configuration +              'None' indicate that no default should be set if the configuration +              does not have the configuration section + +        """          for local_name, (config_name, multiple, default) in mapping.items():              if multiple:                  self.get_values(local_name, config_name, default) @@ -114,12 +251,21 @@ class ConfigurationState (Config):                  self.get_value(local_name, config_name, default)      def remove_default (self,*options): +        """ +        remove all the values which were not changed from the default +        """          for option in options:              if self.exists(option) and self_return_value(option) != self.default[option]:                  continue              del self.options[option]      def to_dict (self): +        """ +        provide a dictionary with the generated data for the configuration +        options: the configuration value for the key +        changes: per key how they changed from the previous configuration +        actions: per changes all the options which were changed +        """          # as we have to use a dict() for the API for verify and apply the options          return {              'options': self.options, diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 1f9636729..b9bfb242a 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -18,11 +18,12 @@ import os  from sys import exit  from copy import deepcopy +from netifaces import interfaces  from vyos.config import Config  from vyos.ifconfig import VXLANIf, Interface +from vyos.util import is_bridge_member  from vyos import ConfigError -from netifaces import interfaces  default_config_data = {      'address': [], @@ -148,7 +149,13 @@ def get_config():  def verify(vxlan):      if vxlan['deleted']: -        # bail out early +        interface = vxlan['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None      if vxlan['mtu'] < 1500: diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 4fa0dd8c0..54121a6c1 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -24,7 +24,7 @@ from netifaces import interfaces  from vyos import ConfigError  from vyos.config import Config  from vyos.configdict import list_diff -from vyos.util import run +from vyos.util import run, is_bridge_member  from vyos.ifconfig import WireGuardIf  kdir = r'/config/auth/wireguard' @@ -179,6 +179,16 @@ def verify(c):      if not c:          return None +    if c['delete']: +        interface = c['intfc'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge)) +        return None +      if not os.path.exists(c['pk']):          raise ConfigError(              "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 188d0ee22..709085b0f 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -29,7 +29,7 @@ from vyos.configdict import list_diff, vlan_to_dict  from vyos.defaults import directories as vyos_data_dir  from vyos.ifconfig import WiFiIf  from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.util import process_running, chmod_x, chown_file, run +from vyos.util import process_running, chmod_x, chown_file, run, is_bridge_member  from vyos import ConfigError  user = 'root' @@ -554,8 +554,16 @@ def get_config():  def verify(wifi):      if wifi['deleted']: +        interface = wifi['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None +      if wifi['op_mode'] != 'monitor' and not wifi['ssid']:          raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 5e10cfce7..49445aaa4 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -23,7 +23,7 @@ from netifaces import interfaces  from vyos.config import Config  from vyos.defaults import directories as vyos_data_dir -from vyos.util import chown_file, chmod_x, cmd, run +from vyos.util import chown_file, chmod_x, cmd, run, is_bridge_member  from vyos import ConfigError  default_config_data = { @@ -115,6 +115,13 @@ def get_config():  def verify(wwan):      if wwan['deleted']: +        interface = wwan['intf'] +        is_member, bridge = is_bridge_member(interface) +        if is_member: +            # can not use a f'' formatted-string here as bridge would not get +            # expanded in the print statement +            raise ConfigError('Can not delete interface "{0}" as it ' \ +                              'is a member of bridge "{1}"!'.format(interface, bridge))          return None      if not wwan['apn']: diff --git a/src/migration-scripts/https/1-to-2 b/src/migration-scripts/https/1-to-2 new file mode 100755 index 000000000..b1cf37ea6 --- /dev/null +++ b/src/migration-scripts/https/1-to-2 @@ -0,0 +1,54 @@ +#!/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/>. + +# * Move 'api virtual-host' list to 'api-restrict virtual-host' so it +#   is owned by https.py instead of http-api.py + +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) + +old_base = ['service', 'https', 'api', 'virtual-host'] +if not config.exists(old_base): +    # Nothing to do +    sys.exit(0) +else: +    new_base = ['service', 'https', 'api-restrict', 'virtual-host'] +    config.set(new_base) + +    names = config.return_values(old_base) +    for name in names: +        config.set(new_base, value=name, replace=False) + +    config.delete(old_base) + +    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)  | 
