neutron/neutron/agent/linux/tc_lib.py

364 lines
13 KiB
Python

# Copyright 2016 OVH SAS
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import math
import re
from neutron_lib import exceptions
from oslo_log import log as logging
from neutron._i18n import _
from neutron.agent.linux import ip_lib
from neutron.common import constants
from neutron.services.qos import qos_consts
LOG = logging.getLogger(__name__)
ROOT_QDISC = "root"
INGRESS_QDISC = "ingress"
INGRESS_QDISC_HEX = "ffff:fff1"
INGRESS_QDISC_HANDLE = "ffff:"
QDISC_TYPE_HTB = "htb"
QDISC_TYPE_DEFAULT = "pfifo_fast"
SI_BASE = 1000
IEC_BASE = 1024
BW_LIMIT_UNIT = "kbit" # kilobits per second in tc's notation
BURST_UNIT = "kbit" # kilobits in tc's notation
# Those are RATES (bits per second) and SIZE (bytes) unit names from tc manual
UNITS = {
"k": 1,
"m": 2,
"g": 3,
"t": 4
}
class InvalidUnit(exceptions.NeutronException):
message = _("Unit name '%(unit)s' is not valid.")
class InvalidPolicyClassParameters(exceptions.NeutronException):
message = _("'rate' or 'ceil' parameters must be defined")
def kilobits_to_bits(value, base):
return value * base
def bits_to_kilobits(value, base):
return int(math.ceil(float(value) / base))
def bytes_to_bits(value):
return value * 8
def bits_to_bytes(value):
return int(value / 8)
def convert_to_kilo(value, base):
value = value.lower()
if "bit" in value:
input_in_bits = True
value = value.replace("bit", "")
else:
input_in_bits = False
value = value.replace("b", "")
# if it is now bare number then it is in bits, so we return it simply
if value.isdigit():
value = int(value)
if input_in_bits:
return bits_to_kilobits(value, base)
else:
bits_value = bytes_to_bits(value)
return bits_to_kilobits(bits_value, base)
unit = value[-1:]
if unit not in UNITS.keys():
raise InvalidUnit(unit=unit)
val = int(value[:-1])
if input_in_bits:
bits_value = val * (base ** UNITS[unit])
else:
bits_value = bytes_to_bits(val * (base ** UNITS[unit]))
return bits_to_kilobits(bits_value, base)
class TcCommand(ip_lib.IPDevice):
def _execute_tc_cmd(self, cmd, **kwargs):
cmd = ['tc'] + cmd
ip_wrapper = ip_lib.IPWrapper(self.namespace)
return ip_wrapper.netns.execute(cmd, run_as_root=True, **kwargs)
@staticmethod
def get_ingress_qdisc_burst_value(bw_limit, burst_limit):
"""Return burst value used in ingress qdisc.
If burst value is not specified given than it will be set to default
rate to ensure that limit for TCP traffic will work well
"""
if not burst_limit:
return int(float(bw_limit) * qos_consts.DEFAULT_BURST_RATE)
return burst_limit
def set_bw(self, max, burst, min, direction):
max = kilobits_to_bits(max, SI_BASE) if max else max
burst = (bits_to_bytes(kilobits_to_bits(burst, IEC_BASE)) if burst
else burst)
min = kilobits_to_bits(min, SI_BASE) if min else min
if direction == constants.EGRESS_DIRECTION:
return self._set_ingress_bw(max, burst, min)
else:
raise NotImplementedError()
def delete_bw(self, direction):
if direction == constants.EGRESS_DIRECTION:
return self._delete_ingress()
else:
raise NotImplementedError()
def get_limits(self, direction):
if direction == constants.EGRESS_DIRECTION:
return self._get_ingress_limits()
else:
raise NotImplementedError()
def _set_ingress_bw(self, max, burst, min):
self._add_policy_qdisc(INGRESS_QDISC, INGRESS_QDISC_HANDLE)
self._configure_ifb(max=max, burst=burst, min=min)
def _delete_ingress(self):
ifb = self._find_mirrored_ifb()
if ifb:
self._del_ifb(ifb)
self._del_policy_qdisc(INGRESS_QDISC)
def _add_policy_qdisc(self, parent, handle, qdisc_type=None, dev=None):
def check_qdisc(qdisc, qdisc_type, handle, parent, device):
if not qdisc or qdisc.get('type') == QDISC_TYPE_DEFAULT:
return False
elif ((qdisc_type and (qdisc.get('type') != qdisc_type or
qdisc.get('handle') != handle)) or
(not qdisc_type and qdisc.get('handle') != handle)):
self._del_policy_qdisc(parent, dev=device)
return False
return True
device = str(dev) if dev else self.name
qdisc = self._show_policy_qdisc(parent, dev=device)
if check_qdisc(qdisc, qdisc_type, handle, parent, device):
return
cmd = ['qdisc', 'add', 'dev', device]
if parent in [ROOT_QDISC, INGRESS_QDISC]:
cmd += [parent]
else:
cmd += ['parent', parent]
cmd += ['handle', handle]
if qdisc_type:
cmd += [qdisc_type]
LOG.debug("Add policy qdisc cmd: %s", cmd)
return self._execute_tc_cmd(cmd)
def _del_policy_qdisc(self, parent, dev=None):
device = str(dev) if dev else self.name
if not self._show_policy_qdisc(parent, dev=device):
return
cmd = ['qdisc', 'del', 'dev', device]
if parent in [ROOT_QDISC, INGRESS_QDISC]:
cmd += [parent]
else:
cmd += ['parent', parent]
LOG.debug("Delete policy qdisc cmd: %s", cmd)
self._execute_tc_cmd(cmd)
def _list_policy_qdisc(self, dev=None):
device = str(dev) if dev else self.name
cmd = ['qdisc', 'show', 'dev', device]
LOG.debug("List policy qdisc cmd: %s", cmd)
result = self._execute_tc_cmd(cmd)
pat = re.compile(r'qdisc (\w+) (\w+\:) (root|parent (\w*\:\w+))')
qdiscs = collections.defaultdict(dict)
for match in (pat.match(line) for line in result.splitlines()
if pat.match(line)):
qdisc = {}
qdisc['type'] = match.groups()[0]
qdisc['handle'] = match.groups()[1]
if match.groups()[2] == ROOT_QDISC:
qdisc['parentid'] = ROOT_QDISC
else:
qdisc['parentid'] = match.groups()[3]
qdisc_ref = INGRESS_QDISC if qdisc['parentid'] == \
INGRESS_QDISC_HEX else qdisc['parentid']
qdiscs[qdisc_ref] = qdisc
LOG.debug("List of policy qdiscs: %s", qdiscs)
return qdiscs
def _show_policy_qdisc(self, parent, dev=None):
device = str(dev) if dev else self.name
return self._list_policy_qdisc(device).get(parent)
def _add_policy_class(self, parent, classid, qdisc_type, rate=None,
ceil=None, burst=None, dev=None):
"""Add new TC class"""
device = str(dev) if dev else self.name
policy = self._show_policy_class(classid, dev=device)
if policy:
rate = (kilobits_to_bits(policy['rate'], SI_BASE) if not rate
else rate)
ceil = (kilobits_to_bits(policy['ceil'], SI_BASE) if not ceil
else ceil)
burst = (bits_to_bytes(kilobits_to_bits(policy['burst'], IEC_BASE))
if not burst else burst)
if not rate and not ceil:
raise InvalidPolicyClassParameters
if not rate:
rate = ceil
cmd = self._cmd_policy_class(classid, qdisc_type, rate, device, parent,
ceil, burst)
LOG.debug("Add/replace policy class cmd: %s", cmd)
return self._execute_tc_cmd(cmd)
def _cmd_policy_class(self, classid, qdisc_type, rate, device, parent,
ceil, burst):
cmd = ['class', 'replace', 'dev', device]
if parent:
cmd += ['parent', parent]
rate = 8 if rate < 8 else rate
cmd += ['classid', classid, qdisc_type, 'rate', rate]
if ceil:
ceil = rate if ceil < rate else ceil
cmd += ['ceil', ceil]
if burst:
cmd += ['burst', burst]
return cmd
def _list_policy_class(self, dev=None):
device = str(dev) if dev else self.name
cmd = ['class', 'show', 'dev', device]
result = self._execute_tc_cmd(cmd, check_exit_code=False)
if not result:
return {}
classes = collections.defaultdict(dict)
pat = re.compile(r'class (\S+) ([0-9a-fA-F]+\:[0-9a-fA-F]+) '
r'(root|parent ([0-9a-fA-F]+\:[0-9a-fA-F]+))'
r'( prio ([0-9]+))* rate (\w+) ceil (\w+) burst (\w+)'
r' cburst (\w+)')
for match in (pat.match(line) for line in result.splitlines()
if pat.match(line)):
_class = {}
_class['type'] = match.groups()[0]
classid = match.groups()[1]
if match.groups()[2] == ROOT_QDISC:
_class['parentid'] = None
else:
_class['parentid'] = match.groups()[3]
_class['prio'] = match.groups()[5]
_class['rate'] = convert_to_kilo(match.groups()[6], SI_BASE)
_class['ceil'] = convert_to_kilo(match.groups()[7], SI_BASE)
_class['burst'] = convert_to_kilo(match.groups()[8], IEC_BASE)
_class['cburst'] = convert_to_kilo(match.groups()[9], IEC_BASE)
classes[classid] = _class
LOG.debug("Policy classes: %s", classes)
return classes
def _show_policy_class(self, classid, dev=None):
device = str(dev) if dev else self.name
return self._list_policy_class(device).get(classid)
def _add_policy_filter(self, parent, protocol, filter, dev=None,
action=None):
"""Add a new filter"""
device = str(dev) if dev else self.name
cmd = ['filter', 'add', 'dev', device, 'parent', parent]
cmd += ['protocol'] + protocol
cmd += filter
if action:
cmd += ['action'] + action
LOG.debug("Add policy filter cmd: %s", cmd)
return self._execute_tc_cmd(cmd)
def _list_policy_filters(self, parent, dev=None):
"""Returns the output of showing the filters in a device"""
device = dev if dev else self.name
cmd = ['filter', 'show', 'dev', device, 'parent', parent]
LOG.debug("List policy filter cmd: %s", cmd)
return self._execute_tc_cmd(cmd)
def _add_ifb(self, dev_name):
"""Create a new IFB device"""
ns_ip = ip_lib.IPWrapper(namespace=self.namespace)
if self._find_mirrored_ifb():
ifb = ip_lib.IPDevice(dev_name, namespace=self.namespace)
if not ifb.exists():
self._del_ifb(dev_name=dev_name)
ifb = ns_ip.add_ifb(dev_name)
else:
self._del_ifb(dev_name=dev_name)
ifb = ns_ip.add_ifb(dev_name)
ifb.disable_ipv6()
ifb.link.set_up()
return ifb
def _del_ifb(self, dev_name):
"""Delete a IFB device"""
ns_ip = ip_lib.IPWrapper(namespace=self.namespace)
devices = ns_ip.get_devices(exclude_loopback=True)
for device in (dev for dev in devices if dev.name == dev_name):
ns_ip.del_ifb(device.name)
def _find_mirrored_ifb(self):
"""Return the name of the IFB device where the traffic is mirrored"""
ifb_name = self.name.replace("tap", "ifb")
ifb = ip_lib.IPDevice(ifb_name, namespace=self.namespace)
if not ifb.exists():
return None
return ifb_name
def _configure_ifb(self, max=None, burst=None, min=None):
ifb = self._find_mirrored_ifb()
if not ifb:
ifb = self.name.replace("tap", "ifb")
self._add_ifb(ifb)
protocol = ['all', 'u32']
filter = ['match', 'u32', '0', '0']
action = ['mirred', 'egress', 'redirect', 'dev', '%s' % ifb]
self._add_policy_filter(INGRESS_QDISC_HANDLE, protocol, filter,
dev=self.name, action=action)
self._add_policy_qdisc(ROOT_QDISC, "1:", qdisc_type=QDISC_TYPE_HTB,
dev=ifb)
self._add_policy_class("1:", "1:1", QDISC_TYPE_HTB, rate=min,
ceil=max, burst=burst, dev=ifb)
def _get_ingress_limits(self):
ifb = self._find_mirrored_ifb()
if ifb:
policy = self._show_policy_class("1:1", dev=ifb)
if policy:
return policy['ceil'], policy['burst'], policy['rate']
return None, None, None