Last active
August 13, 2019 07:55
-
-
Save Arano-kai/c42369433fffadbe5564360f1d6814aa to your computer and use it in GitHub Desktop.
Vlan propagation (gvrp, mvrp) support for oVirt.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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