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) |