202 lines
8.3 KiB
Python
202 lines
8.3 KiB
Python
#!/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
|
|
|
|
OPENVSWITCH_OVS_VSCTL = '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'),
|
|
}
|
|
|
|
|
|
class OvsDbServerNotRunning(Exception):
|
|
pass
|
|
|
|
|
|
class OvsDbServerNotInstalled(Exception):
|
|
pass
|
|
|
|
|
|
def _ovs_vsctl_path(): # pragma: nocover
|
|
if os.path.exists('/usr/bin/'+OPENVSWITCH_OVS_VSCTL) or \
|
|
not os.path.exists('/snap/bin/'+OPENVSWITCH_OVS_VSCTL):
|
|
return '/usr/bin/'+OPENVSWITCH_OVS_VSCTL
|
|
else:
|
|
return '/snap/bin/'+OPENVSWITCH_OVS_VSCTL
|
|
|
|
|
|
OVS_VSCTL_PATH = _ovs_vsctl_path()
|
|
|
|
|
|
def _ovs_installed(): # pragma: nocover
|
|
return os.path.exists(OVS_VSCTL_PATH)
|
|
|
|
|
|
def _ovs_active(): # pragma: nocover
|
|
return subprocess.call([OVS_VSCTL_PATH, 'show'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
|
|
|
|
|
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 = [OVS_VSCTL_PATH, '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 = [OVS_VSCTL_PATH, '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 = [OVS_VSCTL_PATH, '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 = [OVS_VSCTL_PATH, get_cmd]
|
|
args_del = [OVS_VSCTL_PATH, 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([OVS_VSCTL_PATH, '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 _ovs_installed():
|
|
raise OvsDbServerNotInstalled("Cannot apply OpenvSwitch cleanup: ovs-vsctl is 'not-found'")
|
|
|
|
if not _ovs_active():
|
|
raise OvsDbServerNotRunning('OpenvSwitch database is not running')
|
|
|
|
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
|
|
# 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([OVS_VSCTL_PATH, '--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([OVS_VSCTL_PATH, 'br-exists', iface]).returncode > 0:
|
|
subprocess.check_call([OVS_VSCTL_PATH, '--if-exists', 'del-bond-iface', iface])
|
|
else:
|
|
subprocess.check_call([OVS_VSCTL_PATH, '--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([OVS_VSCTL_PATH, '--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)
|