Skip to content

Instantly share code, notes, and snippets.

@Arano-kai
Last active August 13, 2019 07:55
Show Gist options
  • Save Arano-kai/c42369433fffadbe5564360f1d6814aa to your computer and use it in GitHub Desktop.
Save Arano-kai/c42369433fffadbe5564360f1d6814aa to your computer and use it in GitHub Desktop.
Vlan propagation (gvrp, mvrp) support for oVirt.
#! /usr/bin/env python2.7
'''
Add support of vlan propagation protocols to oVirt node.
One can specify custom key 'vlan_propagation' in host network setup
to one or [\s,] separated sequence of supported protocols.
Currently only 'legasy' switch type is supported with 'gvrp' and 'mvrp' protocols.
Also, one can use special keyword 'all' to enable all suported protocols.
All unspecified protocols will be disabled.
All unsuported protocols/switches will drop warnings to stderr
(collected by vdsm in '/var/log/vdsm/supervdsm.log').
Changes in data, provided by oVirt/vdsm, will raise 'APIError' and abort execution.
Usage:
1) Create custom key on engine:
1.1) "engine-config -s 'UserDefinedNetworkCustomProperties=vlan_propagation=<regex>' --cver=<cluster_ver>"
where <regex> one of:
* Stricted to supported protocols only:
'^([gGmM][vV][rR][pP]|[aA][lL]{2})([\s,]+([gGmM][vV][rR][pP]|[aA][lL]{2})[\s,]*)*'
* Permissive, since this script do excessive checking:
'^\w+([\s,]+\w+[\s,]*)*$'
Please note, that early defined keys needs to be preserved by using ';' separator.
1.2) "systemctl restart ovirt-engine"
2) Copy this script to each affected node in '/usr/libexec/vdsm/hooks/after_network_setup/' and make it executable.
3) Set custom key in node network configuration.
Implementation:
Protocols controlled by 'ip' (iproute2) command in legasy switch, executed on each vlan.
Also, '/etc/sysconfig/network-scripts/ifcfg-<interface>' is modified accordynly to survive non-vdsm interface resets.
One can impleent support for other switch types by extending 'propagateNets' function.
Set 'logLevel = logging.DEBUG' for verbose logging.
PS: technically, 'before_ifcfg_write' is better place for such things, but oVirt/vdsm provide too little data there.
'''
import os
import sys
import logging
import atexit
import hooking
from pprint import pformat
import re
from collections import OrderedDict
from distutils.spawn import find_executable
from shlex import split as shsplit
logLevel = logging.INFO
class CustomFilter(logging.Filter):
'''Modify log message, if additional keys present.
Usage:
logger.<log_level>(msg, extra={<key>: <data>})
Where:
key - one of supported keys.
data - data to modify.
Supported keys:
'pp' - pretty print data in human readable format;
printed from newline with added indent.
'inv' - add 'Args:' at msg tail, then act as 'pp'
'''
def filter(self, record):
#init arguments wrapper
if hasattr(record, 'inv') and len(record.inv) > 0:
record.msg = record.msg + ' Args:'
record.pp = record.inv
#prettify provided data for humans
if hasattr(record, 'pp') and len(record.pp) > 0:
record.msg = record.msg + '\n\t' + pformat(record.pp, width=1).replace('\n', '\n\t')
return super(CustomFilter, self).filter(record)
class APIError(Exception):
'''Store json data for in-place debug on API changes'''
def __init__(self, obj, data = None):
self.logger = logging.getLogger('.'.join([logger.name, self.__class__.__name__]))
self.logger.debug('Invoked.', extra={'inv': locals()})
if isinstance(obj, Exception):
msg = obj.message
elif isinstance(obj, str):
msg = obj
else:
raise TypeError('1st arg unsupported type: \'{}\', must be \'(inst)Exception\' or \'str\'!'.format(type(obj)))
super(APIError, self).__init__(msg)
self.data = data
class SimpleConfig(OrderedDict):
'''Manipulate plain configs as od.
Comments are preserved via inner class obj as key.
'''
class SimpleConfigComment:
'''Comment marker'''
pass
def __init__(self, filepath=None, delimeter="=", comment='^\s*[;#]+'):
self.logger = logging.getLogger('.'.join([logger.name, self.__class__.__name__]))
self.logger.debug('Invoked.', extra={'inv': locals()})
super(SimpleConfig, self).__init__()
self.delimeter = delimeter
self.comment = re.compile(comment)
if filepath:
self.read(filepath)
def read(self, filepath):
self.logger.debug('Invoked.', extra={'inv': locals()})
with open(filepath, 'r') as fd:
for line in fd:
line = line.rstrip('\n')
if self.comment.search(line):
self[self.SimpleConfigComment()] = line
else:
line = line.split(self.delimeter)
k = line.pop(0)
self[k] = self.delimeter.join(line)
self.logger.debug('Data loaded:', extra={'pp': self.viewitems()})
def write(self, filepath):
self.logger.debug('Invoked.', extra={'inv': locals()})
with open(filepath, 'w') as fd:
for k, v in self.iteritems():
if isinstance(k, self.SimpleConfigComment):
fd.write(v + '\n')
else:
fd.write(self.delimeter.join([k, v]) + '\n')
def main():
try:
networks = parseNets(getNets())
for net, opts in networks.iteritems():
logger.info('Net \'%s\'(%s): processing begin.', net, opts['stype'])
if opts['stype'] == 'legacy':
propagateNetLegasy('.'.join([opts['ifname'], opts['vlan']]), opts['protos'])
#Unsupported switch clause
else:
logger.warning('Net \'%s\': ignoring network: unsupported switch: \'%s\'.',
net, opts['stype'])
except APIError as e:
logger.exception('API changed or hook misplaced?')
if e.data:
logger.error('Data struct provided:', extra={'pp': e.data})
sys.exit(1)
except:
#Do not interrup other vdsm hooks
logger.exception('Unhandled exception!')
sys.exit(1)
sys.exit(0)
def propagateNetLegasy(interface, protocols):
'''Manage propagation protocols status
Args:
interface: <str> - vlan interface to act on.
protos: <set> - protocols to activate in lowercase, deactivate otherwise;
supported: {'gvrp', 'mvrp', 'all'};
unsupported will be ignored with warn.
Return: None
'''
logger.debug('Invoked.', extra={'inv': locals()})
ipbin = find_executable('ip')
if ipbin is None or len(ipbin) < 1:
raise OSError ('\'iproute2\' required to support \'legasy\' switch type!')
protoSupported = {'gvrp', 'mvrp'}
pathIfcfgBase = os.path.sep + os.path.sep.join(['etc', 'sysconfig', 'network-scripts', 'ifcfg-'])
protoEnabled = protoSupported.union({'all'}).intersection(protocols)
if len(protoEnabled) < len(protocols):
logger.warning('Ignoring unsupported protocols: \'%s\'.',
", ".join(protoEnabled.difference(protocols)))
if 'all' in protoEnabled:
protoEnabled = protoSupported
ifcfg = SimpleConfig(pathIfcfgBase + interface)
for proto in protoSupported:
if proto in protoEnabled:
state = 'on'
ifcfg[proto.upper()] = 'on'
else:
state = 'off'
ifcfg.pop(proto.upper(), None)
cmd = '{b} link set dev {i} type vlan {p} {s}'.format(
b=ipbin, i=interface, p=proto, s=state)
logger.debug('About to exec: %s', cmd)
try:
rcode, out, err = hooking.execCmd(shsplit(cmd), sudo=True, execCmdLogger=logger)
if rcode > 0:
raise OSError(err)
except OSError as e:
logger.error('Exec \'%s\'(%s): %s', cmd, str(rcode), e.message)
if "Operation not permitted" in e.message:
logger.error('It seems that \'vdsm\' lacks permission to use \'sudo %s\' without password. '
'Please, adjust sudo rules and try again.', ipbin)
raise
logger.info('Protocol \'%s\': %s', proto, state)
ifcfg.write(pathIfcfgBase + interface)
@atexit.register
def terminate():
logger.info('Hook exit')
def getNets():
'''Retreive networks dictionary from oVirt/vdsm provided data'''
logger.debug('Invoked. Environ:', extra={'pp': os.environ})
dctNets = lambda d: d['request']['networks']
try:
data = hooking.read_json()
logger.debug('data:', extra={'pp': data})
return dctNets(data)
except (KeyError, AttributeError, TypeError) as e:
raise APIError(e, data), None, sys.exc_info()[2]
def parseNets(nets):
'''Convert provided data for internal usage.
Args (only expected shown):
nets - {
'net1': {
'vlan': <int/str> - vlan ID to act on;
missing will ignore network with warn.
'switch: <str> - switch type;
missing will raise 'APIError'.
('bonding'|'nic'): <str> - interface to act on;
at least one must be provided, raise 'APIError' otherwise.
'custom.vlan_propagation': <str> - protocols to activate;
[\s,]+ accepted as separators;
case insensitive;
missing is part of logic.
},
'net2': {...},
...
}
Return:
{
'net1': {
'stype': <str> - switch type in lowercase.
'ifname': <str> - interface name.
'vlan': <str> - vlan id.
'protos': <set> - protocols to activate in lowercase.
},
'net2': {...},
...
}
'''
logger.debug('Invoked.', extra={'inv': locals()})
keyProtos = lambda d: d.get('custom', {}).get('vlan_propagation', '')
keyNicTypes = {'bonding', 'nic'}
sepProtos = re.compile('[\s,]+')
ret = {}
for net, opts in nets.iteritems():
if 'vlan' not in opts.keys() or len(str(opts['vlan'])) < 1:
logger.warning('Net \'%s\': ignoring non-vlan network.', net)
continue
try:
ifname = opts[keyNicTypes.intersection(set(opts.keys())).pop()]
except KeyError:
raise (APIError('Net \'{net}\': '
'at least one of the following keys must be present: {keysreq}'.format(
net=net, keysreq=keyNicTypes), nets[net]),
None, sys.exc_info()[2])
try:
stype = opts['switch'].lower()
except KeyError as e:
raise(APIError(e, nets[net]), None, sys.exc_info()[2])
protos = set(sepProtos.split(keyProtos(opts).lower()))
protos.discard('')
ret[net] = dict(stype=stype, ifname=ifname, vlan=str(opts['vlan']), protos=protos)
return ret
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.addFilter(CustomFilter())
nameSelf = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[-2::1])
formatter = logging.Formatter('Hook|{}::%(levelname)s::%(asctime)s::%(name)s[%(funcName)s]: %(message)s'.format(nameSelf))
ch.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.setLevel(logLevel)
logger.addHandler(ch)
logger.info('')
logger.info('Hook init')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment