1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
#!/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
from .utils import systemctl_is_active, systemctl_is_installed
OPENVSWITCH_OVS_VSCTL = '/usr/bin/ovs-vsctl'
OPENVSWITCH_OVSDB_SERVER_UNIT = 'ovsdb-server.service'
# 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'),
}
class OvsDbServerNotRunning(Exception):
pass
class OvsDbServerNotInstalled(Exception):
pass
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
cmd = [OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, value]
logging.debug('Running: %s' % ' '.join(cmd))
subprocess.check_call(cmd)
elif default and default != value:
# reset to default, if its not the default already
cmd = [OPENVSWITCH_OVS_VSCTL, 'set', type, iface, '%s=%s' % (column, default)]
logging.debug('Running: %s' % ' '.join(cmd))
subprocess.check_call(cmd)
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
cmd = [OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, '%s=\"%s\"' % (key, value)]
logging.debug('Running: %s' % ' '.join(cmd))
subprocess.check_call(cmd)
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, text=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 unknown 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, np_interface_dict):
assert isinstance(np_interface_dict, dict)
np_def = np_interface_dict.get(iface, None)
return np_def and np_def.backend == 'OpenVSwitch'
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.
"""
if not systemctl_is_installed(OPENVSWITCH_OVSDB_SERVER_UNIT):
raise OvsDbServerNotInstalled("Cannot apply OVS cleanup: %s is 'not-found'" %
OPENVSWITCH_OVSDB_SERVER_UNIT)
if not systemctl_is_active(OPENVSWITCH_OVSDB_SERVER_UNIT):
raise OvsDbServerNotRunning('{} is not running'.format(OPENVSWITCH_OVSDB_SERVER_UNIT))
config_manager.parse()
ovs_ifaces = set()
for i in config_manager.netdefs.keys():
if (is_ovs_interface(i, config_manager.netdefs)):
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]],
text=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],
text=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')
|