View file File name : ovs.py Content :#!/usr/bin/python3 # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas 'slyon' Märdian <lukas.maerdian@canonical.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # 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 logging import os import subprocess import re OPENVSWITCH_OVS_VSCTL = '/usr/bin/ovs-vsctl' # Defaults for non-optional settings, as defined here: # http://www.openvswitch.org/ovs-vswitchd.conf.db.5.pdf DEFAULTS = { # Mandatory columns: 'mcast_snooping_enable': 'false', 'rstp_enable': 'false', } GLOBALS = { # Global commands: 'set-ssl': ('del-ssl', 'get-ssl'), 'set-fail-mode': ('del-fail-mode', 'get-fail-mode'), 'set-controller': ('del-controller', 'get-controller'), } def _del_col(type, iface, column, value): """Cleanup values from a column (i.e. "column=value")""" default = DEFAULTS.get(column) if default is None: # removes the exact value only if it was set by netplan subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, value]) elif default and default != value: # reset to default, if its not the default already subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'set', type, iface, '%s=%s' % (column, default)]) def _del_dict(type, iface, column, key, value): """Cleanup values from a dictionary (i.e. "column:key=value")""" # removes the exact value only if it was set by netplan subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, key, _escape_colon(value)]) # for ovsdb remove: column key's value can not contain bare ':', need to escape with '\' def _escape_colon(literal): return re.sub(r'([^\\]):', r'\g<1>\:', literal) def _del_global(type, iface, key, value): """Cleanup commands from the global namespace""" del_cmd, get_cmd = GLOBALS.get(key, (None, None)) if del_cmd == 'del-ssl': iface = None if del_cmd: args_get = [OPENVSWITCH_OVS_VSCTL, get_cmd] args_del = [OPENVSWITCH_OVS_VSCTL, del_cmd] if iface: args_get.append(iface) args_del.append(iface) # Check the current value of a global command and compare it to the tag-value, e.g.: # * get-ssl: netplan/global/set-ssl=/private/key.pem,/another/cert.pem,/some/ca-cert.pem # Private key: /private/key.pem # Certificate: /another/cert.pem # CA Certificate: /some/ca-cert.pem # Bootstrap: false # * get-fail-mode: netplan/global/set-fail-mode=secure # secure # * get-controller: netplan/global/set-controller=tcp:127.0.0.1:1337,unix:/some/socket # tcp:127.0.0.1:1337 # unix:/some/socket out = subprocess.check_output(args_get, universal_newlines=True) # Clean it only if the exact same value(s) were set by netplan. # Don't touch it if other values were set by another integration. if all(item in out for item in value.split(',')): subprocess.check_call(args_del) else: raise Exception('Reset command unkown for:', key) def clear_setting(type, iface, setting, value): """Check if this setting is in a dict or a colum and delete accordingly""" split = setting.split('/', 2) col = split[1] if col == 'global' and len(split) > 2: _del_global(type, iface, split[2], value) elif len(split) > 2: _del_dict(type, iface, split[1], split[2], value) else: _del_col(type, iface, split[1], value) # Cleanup the tag itself (i.e. "netplan/column[/key]") subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, 'external-ids', setting]) def is_ovs_interface(iface, interfaces): assert isinstance(interfaces, dict) if not isinstance(interfaces.get(iface), dict): logging.debug('Ignoring special key: {} ({})'.format(iface, interfaces.get(iface))) return False elif interfaces.get(iface, {}).get('openvswitch') is not None: return True else: return any(is_ovs_interface(i, interfaces) for i in interfaces.get(iface, {}).get('interfaces', [])) def apply_ovs_cleanup(config_manager, ovs_old, ovs_current): # pragma: nocover (covered in autopkgtest) """ Query OpenVSwitch state through 'ovs-vsctl' and filter for netplan=true tagged ports/bonds and bridges. Delete interfaces which are not defined in the current configuration. Also filter for individual settings tagged netplan/<column>[/<key]=value in external-ids and clear them if they have been set by netplan. """ config_manager.parse() ovs_ifaces = set() for i in config_manager.interfaces.keys(): if (is_ovs_interface(i, config_manager.interfaces)): ovs_ifaces.add(i) # Tear down old OVS interfaces, not defined in the current config. # Use 'del-br' on the Interface table, to delete any netplan created VLAN fake bridges. # Use 'del-bond-iface' on the Interface table, to delete netplan created patch port interfaces if os.path.isfile(OPENVSWITCH_OVS_VSCTL): # Step 1: Delete all interfaces, which are not part of the current OVS config for t in (('Port', 'del-port'), ('Bridge', 'del-br'), ('Interface', 'del-br')): out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t[0]], universal_newlines=True) for line in out.splitlines(): if 'netplan=true' in line: iface = line.split(',')[0] # Skip cleanup if this OVS interface is part of the current netplan OVS config if iface in ovs_ifaces: continue if t[0] == 'Interface' and subprocess.run([OPENVSWITCH_OVS_VSCTL, 'br-exists', iface]).returncode > 0: subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', 'del-bond-iface', iface]) else: subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', t[1], iface]) # Step 2: Clean up the settings of the remaining interfaces for t in ('Port', 'Bridge', 'Interface', 'Open_vSwitch', 'Controller'): cols = 'name,external-ids' if t == 'Open_vSwitch': cols = 'external-ids' elif t == 'Controller': cols = '_uuid,external-ids' # handle _uuid as if it would be the iface 'name' out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t], universal_newlines=True) for line in out.splitlines(): if 'netplan/' in line: iface = '.' extids = line if t != 'Open_vSwitch': iface, extids = line.split(',', 1) # Check each line (interface) if it contains any netplan tagged settings, e.g.: # ovs0,"iface-id=myhostname netplan=true netplan/external-ids/iface-id=myhostname" # ovs1,"netplan=true netplan/global/set-fail-mode=standalone netplan/mcast_snooping_enable=false" for entry in extids.strip('"').split(' '): if entry.startswith('netplan/') and '=' in entry: setting, val = entry.split('=', 1) clear_setting(t, iface, setting, val) # Show the warning only if we are or have been working with OVS definitions elif ovs_old or ovs_current: logging.warning('ovs-vsctl is missing, cannot tear down old OpenVSwitch interfaces')