libvirt/src/util/virnetdevbandwidth.c
Michal Privoznik cea3715b2e QoS: Set classes and filters in proper direction
Similarly to previous patch, for some types of interface domain
and host are on the same side of RX/TX barrier. In that case, we
need to set up the QoS differently. Well, swapped.

Signed-off-by: Michal Privoznik <mprivozn@redhat.com>
Reviewed-by: John Ferlan <jferlan@redhat.com>
2017-10-05 09:16:05 +02:00

770 lines
25 KiB
C

/*
* Copyright (C) 2009-2015 Red Hat, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* <http://www.gnu.org/licenses/>.
*
* Authors:
* Michal Privoznik <mprivozn@redhat.com>
* Daniel P. Berrange <berrange@redhat.com>
*/
#include <config.h>
#include <unistd.h>
#include "virnetdevbandwidth.h"
#include "vircommand.h"
#include "viralloc.h"
#include "virerror.h"
#include "virstring.h"
#include "virutil.h"
#define VIR_FROM_THIS VIR_FROM_NONE
void
virNetDevBandwidthFree(virNetDevBandwidthPtr def)
{
if (!def)
return;
VIR_FREE(def->in);
VIR_FREE(def->out);
VIR_FREE(def);
}
static void
virNetDevBandwidthCmdAddOptimalQuantum(virCommandPtr cmd,
const virNetDevBandwidthRate *rate)
{
const unsigned long long mtu = 1500;
unsigned long long r2q;
/* When two or more classes compete for unused bandwidth they are each
* given some number of bytes before serving other competing class. This
* number is called quantum. It's advised in HTB docs that the number
* should be equal to MTU. The class quantum is computed from its rate
* divided by global r2q parameter. However, if rate is too small the
* default value will not suffice and thus we must provide our own value.
* */
r2q = rate->average * 1024 / 8 / mtu;
if (!r2q)
r2q = 1;
virCommandAddArg(cmd, "quantum");
virCommandAddArgFormat(cmd, "%llu", r2q);
}
/**
* virNetDevBandwidthManipulateFilter:
* @ifname: interface to operate on
* @ifmac_ptr: MAC of the interface to create filter over
* @id: filter ID
* @class_id: where to place traffic
* @remove_old: whether to remove the filter
* @create_new: whether to create the filter
*
* TC filters are as crucial for traffic shaping as QDiscs. While
* QDiscs act like black boxes deciding which packets should be
* held up and which should be sent immediately, it's the filter
* that places a packet into the box. So, we may end up
* constructing a set of filters on a single device (e.g. a
* bridge) and filter the traffic into QDiscs based on the
* originating vNET device.
*
* Long story short, @ifname is the interface where the filter
* should be created. The @ifmac_ptr is the MAC address for which
* the filter should be created (usually different to the MAC
* address of @ifname). Then, like everything - even filters have
* an @id which should be unique (per @ifname). And @class_id
* tells into which QDisc should filter place the traffic.
*
* This function can be used for both, removing stale filter
* (@remove_old set to true) and creating new one (@create_new
* set to true). Both at once for the same price!
*
* Returns: 0 on success,
* -1 otherwise (with error reported).
*/
static int ATTRIBUTE_NONNULL(1)
virNetDevBandwidthManipulateFilter(const char *ifname,
const virMacAddr *ifmac_ptr,
unsigned int id,
const char *class_id,
bool remove_old,
bool create_new)
{
int ret = -1;
char *filter_id = NULL;
virCommandPtr cmd = NULL;
unsigned char ifmac[VIR_MAC_BUFLEN];
char *mac[2] = {NULL, NULL};
if (!(remove_old || create_new)) {
virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
_("filter creation API error"));
goto cleanup;
}
/* u32 filters must have 800:: prefix. Don't ask. */
if (virAsprintf(&filter_id, "800::%u", id) < 0)
goto cleanup;
if (remove_old) {
int cmd_ret = 0;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "filter", "del", "dev", ifname,
"prio", "2", "handle", filter_id, "u32", NULL);
if (virCommandRun(cmd, &cmd_ret) < 0)
goto cleanup;
}
if (create_new) {
virMacAddrGetRaw(ifmac_ptr, ifmac);
if (virAsprintf(&mac[0], "0x%02x%02x%02x%02x", ifmac[2],
ifmac[3], ifmac[4], ifmac[5]) < 0 ||
virAsprintf(&mac[1], "0x%02x%02x", ifmac[0], ifmac[1]) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
/* Okay, this not nice. But since libvirt does not necessarily track
* interface IP address(es), and tc fw filter simply refuse to use
* ebtables marks, we need to use u32 selector to match MAC address.
* If libvirt will ever know something, remove this FIXME
*/
virCommandAddArgList(cmd, "filter", "add", "dev", ifname, "protocol", "ip",
"prio", "2", "handle", filter_id, "u32",
"match", "u16", "0x0800", "0xffff", "at", "-2",
"match", "u32", mac[0], "0xffffffff", "at", "-12",
"match", "u16", mac[1], "0xffff", "at", "-14",
"flowid", class_id, NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
}
ret = 0;
cleanup:
VIR_FREE(mac[1]);
VIR_FREE(mac[0]);
VIR_FREE(filter_id);
virCommandFree(cmd);
return ret;
}
/**
* virNetDevBandwidthSet:
* @ifname: on which interface
* @bandwidth: rates to set (may be NULL)
* @hierarchical_class: whether to create hierarchical class
* @swapped: true if IN/OUT should be set contrariwise
*
* This function enables QoS on specified interface
* and set given traffic limits for both, incoming
* and outgoing traffic. Any previous setting get
* overwritten. If @hierarchical_class is TRUE, create
* hierarchical class. It is used to guarantee minimal
* throughput ('floor' attribute in NIC).
*
* If @swapped is set, the IN part of @bandwidth is set on
* @ifname's TX, and vice versa. If it is not set, IN is set on
* RX and OUT on TX. This is because for some types of interfaces
* domain and the host live on the same side of the interface (so
* domain's RX/TX is host's RX/TX), and for some it's swapped
* (domain's RX/TX is hosts's TX/RX).
*
* Return 0 on success, -1 otherwise.
*/
int
virNetDevBandwidthSet(const char *ifname,
virNetDevBandwidthPtr bandwidth,
bool hierarchical_class,
bool swapped)
{
int ret = -1;
virNetDevBandwidthRatePtr rx = NULL, tx = NULL; /* From domain POV */
virCommandPtr cmd = NULL;
char *average = NULL;
char *peak = NULL;
char *burst = NULL;
if (!bandwidth) {
/* nothing to be enabled */
ret = 0;
goto cleanup;
}
if (geteuid() != 0) {
virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
_("Network bandwidth tuning is not available"
" in session mode"));
return -1;
}
if (!ifname) {
virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
_("Unable to set bandwidth for interface because "
"device name is unknown"));
return -1;
}
if (swapped) {
rx = bandwidth->out;
tx = bandwidth->in;
} else {
rx = bandwidth->in;
tx = bandwidth->out;
}
virNetDevBandwidthClear(ifname);
if (tx && tx->average) {
if (virAsprintf(&average, "%llukbps", tx->average) < 0)
goto cleanup;
if (tx->peak &&
(virAsprintf(&peak, "%llukbps", tx->peak) < 0))
goto cleanup;
if (tx->burst &&
(virAsprintf(&burst, "%llukb", tx->burst) < 0))
goto cleanup;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "add", "dev", ifname, "root",
"handle", "1:", "htb", "default",
hierarchical_class ? "2" : "1", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
/* If we are creating a hierarchical class, all non guaranteed traffic
* goes to the 1:2 class which will adjust 'rate' dynamically as NICs
* with guaranteed throughput are plugged and unplugged. Class 1:1
* exists so we don't exceed the maximum limit for the network. For each
* NIC with guaranteed throughput a separate classid will be created.
* NB '1:' is just a shorter notation of '1:0'.
*
* To get a picture how this works:
*
* +-----+ +---------+ +-----------+ +-----------+ +-----+
* | | | qdisc | | class 1:1 | | class 1:2 | | |
* | NIC | | def 1:2 | | rate | | rate | | sfq |
* | | --> | | --> | peak | -+-> | peak | --> | |
* +-----+ +---------+ +-----------+ | +-----------+ +-----+
* |
* | +-----------+ +-----+
* | | class 1:3 | | |
* | | rate | | sfq |
* +-> | peak | --> | |
* | +-----------+ +-----+
* ...
* | +-----------+ +-----+
* | | class 1:n | | |
* | | rate | | sfq |
* +-> | peak | --> | |
* +-----------+ +-----+
*
* After the routing decision, when is it clear a packet is to be sent
* via a particular NIC, it is sent to the root qdisc (queueing
* discipline). In this case HTB (Hierarchical Token Bucket). It has
* only one direct child class (with id 1:1) which shapes the overall
* rate that is sent through the NIC. This class has at least one child
* (1:2) which is meant for all non-privileged (non guaranteed) traffic
* from all domains. Then, for each interface with guaranteed
* throughput, a separate class (1:n) is created. Imagine a class is a
* box. Whenever a packet ends up in a class it is stored in this box
* until the kernel sends it, then it is removed from box. Packets are
* placed into boxes based on rules (filters) - e.g. depending on
* destination IP/MAC address. If there is no rule to be applied, the
* root qdisc has a default where such packets go (1:2 in this case).
* Packets come in over and over again and boxes get filled more and
* more. Imagine that kernel sends packets just once a second. So it
* starts to traverse through this tree. It starts with the root qdisc
* and through 1:1 it gets to 1:2. It sends packets up to 1:2's 'rate'.
* Then it moves to 1:3 and again sends packets up to 1:3's 'rate'. The
* whole process is repeated until 1:n is processed. So now we have
* ensured each class its guaranteed bandwidth. If the sum of sent data
* doesn't exceed the 'rate' in 1:1 class, we can go further and send
* more packets. The rest of available bandwidth is distributed to the
* 1:2,1:3...1:n classes by ratio of their 'rate'. As soon as the root
* 'rate' limit is reached or there are no more packets to send, we stop
* sending and wait another second. Each class has an SFQ qdisc which
* shuffles packets in boxes stochastically, so one sender cannot
* starve others.
*
* Therefore, whenever we want to plug in a new guaranteed interface, we
* need to create a new class and adjust the 'rate' of the 1:2 class.
* When unplugging we do the exact opposite - remove the associated
* class, and adjust the 'rate'.
*
* This description is rather long, but it is still a good idea to read
* it before you dig into the code.
*/
if (hierarchical_class) {
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "class", "add", "dev", ifname, "parent",
"1:", "classid", "1:1", "htb", "rate", average,
"ceil", peak ? peak : average, NULL);
virNetDevBandwidthCmdAddOptimalQuantum(cmd, tx);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
}
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "class", "add", "dev", ifname, "parent",
hierarchical_class ? "1:1" : "1:", "classid",
hierarchical_class ? "1:2" : "1:1", "htb",
"rate", average, NULL);
if (peak)
virCommandAddArgList(cmd, "ceil", peak, NULL);
if (burst)
virCommandAddArgList(cmd, "burst", burst, NULL);
virNetDevBandwidthCmdAddOptimalQuantum(cmd, tx);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "add", "dev", ifname, "parent",
hierarchical_class ? "1:2" : "1:1",
"handle", "2:", "sfq", "perturb",
"10", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "filter", "add", "dev", ifname, "parent",
"1:0", "protocol", "all", "prio", "1", "handle",
"1", "fw", "flowid", "1", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
VIR_FREE(average);
VIR_FREE(peak);
VIR_FREE(burst);
}
if (rx) {
if (virAsprintf(&average, "%llukbps", rx->average) < 0)
goto cleanup;
if (virAsprintf(&burst, "%llukb", rx->burst ? rx->burst : rx->average) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "add", "dev", ifname,
"ingress", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
/* Set filter to match all ingress traffic */
virCommandAddArgList(cmd, "filter", "add", "dev", ifname, "parent",
"ffff:", "protocol", "all", "u32", "match", "u32",
"0", "0", "police", "rate", average,
"burst", burst, "mtu", "64kb", "drop", "flowid",
":1", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
}
ret = 0;
cleanup:
virCommandFree(cmd);
VIR_FREE(average);
VIR_FREE(peak);
VIR_FREE(burst);
return ret;
}
/**
* virNetDevBandwidthClear:
* @ifname: on which interface
*
* This function tries to disable QoS on specified interface
* by deleting root and ingress qdisc. However, this may fail
* if we try to remove the default one.
*
* Return 0 on success, -1 otherwise.
*/
int
virNetDevBandwidthClear(const char *ifname)
{
int ret = 0;
int dummy; /* for ignoring the exit status */
virCommandPtr cmd = NULL;
if (!ifname)
return 0;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "del", "dev", ifname, "root", NULL);
if (virCommandRun(cmd, &dummy) < 0)
ret = -1;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "del", "dev", ifname, "ingress", NULL);
if (virCommandRun(cmd, &dummy) < 0)
ret = -1;
virCommandFree(cmd);
return ret;
}
/*
* virNetDevBandwidthCopy:
* @dest: destination
* @src: source (may be NULL)
*
* Returns -1 on OOM error (which gets reported),
* 0 otherwise.
*/
int
virNetDevBandwidthCopy(virNetDevBandwidthPtr *dest,
const virNetDevBandwidth *src)
{
int ret = -1;
*dest = NULL;
if (!src) {
/* nothing to be copied */
return 0;
}
if (VIR_ALLOC(*dest) < 0)
goto cleanup;
if (src->in) {
if (VIR_ALLOC((*dest)->in) < 0)
goto cleanup;
memcpy((*dest)->in, src->in, sizeof(*src->in));
}
if (src->out) {
if (VIR_ALLOC((*dest)->out) < 0) {
VIR_FREE((*dest)->in);
goto cleanup;
}
memcpy((*dest)->out, src->out, sizeof(*src->out));
}
ret = 0;
cleanup:
if (ret < 0) {
virNetDevBandwidthFree(*dest);
*dest = NULL;
}
return ret;
}
bool
virNetDevBandwidthEqual(virNetDevBandwidthPtr a,
virNetDevBandwidthPtr b)
{
if (!a && !b)
return true;
if (!a || !b)
return false;
/* in */
if (a->in) {
if (!b->in)
return false;
if (a->in->average != b->in->average ||
a->in->peak != b->in->peak ||
a->in->floor != b->in->floor ||
a->in->burst != b->in->burst)
return false;
} else if (b->in) {
return false;
}
/*out*/
if (a->out) {
if (!b->out)
return false;
if (a->out->average != b->out->average ||
a->out->peak != b->out->peak ||
a->out->floor != b->out->floor ||
a->out->burst != b->out->burst)
return false;
} else if (b->out) {
return false;
}
return true;
}
/*
* virNetDevBandwidthPlug:
* @brname: name of the bridge
* @net_bandwidth: QoS settings on @brname
* @ifmac_ptr: MAC of interface
* @bandwidth: QoS settings for interface
* @id: unique ID (MUST be greater than 2)
*
* Set bridge part of interface QoS settings, e.g. guaranteed
* bandwidth. @id is an unique ID (among @brname) from which
* other identifiers for class, qdisc and filter are derived.
* However, two classes were already set up (by
* virNetDevBandwidthSet). That's why this @id MUST be greater
* than 2. You may want to keep passed @id, as it is used later
* by virNetDevBandwidthUnplug.
*
* Returns:
* 0 if QoS set successfully
* -1 otherwise.
*/
int
virNetDevBandwidthPlug(const char *brname,
virNetDevBandwidthPtr net_bandwidth,
const virMacAddr *ifmac_ptr,
virNetDevBandwidthPtr bandwidth,
unsigned int id)
{
int ret = -1;
virCommandPtr cmd = NULL;
char *class_id = NULL;
char *qdisc_id = NULL;
char *floor = NULL;
char *ceil = NULL;
char ifmacStr[VIR_MAC_STRING_BUFLEN];
if (id <= 2) {
virReportError(VIR_ERR_INTERNAL_ERROR, _("Invalid class ID %d"), id);
return -1;
}
virMacAddrFormat(ifmac_ptr, ifmacStr);
if (!net_bandwidth || !net_bandwidth->in) {
virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
_("Bridge '%s' has no QoS set, therefore "
"unable to set 'floor' on '%s'"),
brname, ifmacStr);
return -1;
}
if (virAsprintf(&class_id, "1:%x", id) < 0 ||
virAsprintf(&qdisc_id, "%x:", id) < 0 ||
virAsprintf(&floor, "%llukbps", bandwidth->in->floor) < 0 ||
virAsprintf(&ceil, "%llukbps", net_bandwidth->in->peak ?
net_bandwidth->in->peak :
net_bandwidth->in->average) < 0)
goto cleanup;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "class", "add", "dev", brname, "parent", "1:1",
"classid", class_id, "htb", "rate", floor,
"ceil", ceil, NULL);
virNetDevBandwidthCmdAddOptimalQuantum(cmd, bandwidth->in);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "add", "dev", brname, "parent",
class_id, "handle", qdisc_id, "sfq", "perturb",
"10", NULL);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
if (virNetDevBandwidthManipulateFilter(brname, ifmac_ptr, id,
class_id, false, true) < 0)
goto cleanup;
ret = 0;
cleanup:
VIR_FREE(ceil);
VIR_FREE(floor);
VIR_FREE(qdisc_id);
VIR_FREE(class_id);
virCommandFree(cmd);
return ret;
}
/*
* virNetDevBandwidthUnplug:
* @brname: from which bridge are we unplugging
* @id: unique identifier (MUST be greater than 2)
*
* Remove QoS settings from bridge.
*
* Returns 0 on success, -1 otherwise.
*/
int
virNetDevBandwidthUnplug(const char *brname,
unsigned int id)
{
int ret = -1;
int cmd_ret = 0;
virCommandPtr cmd = NULL;
char *class_id = NULL;
char *qdisc_id = NULL;
if (id <= 2) {
virReportError(VIR_ERR_INTERNAL_ERROR, _("Invalid class ID %d"), id);
return -1;
}
if (virAsprintf(&class_id, "1:%x", id) < 0 ||
virAsprintf(&qdisc_id, "%x:", id) < 0)
goto cleanup;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "qdisc", "del", "dev", brname,
"handle", qdisc_id, NULL);
/* Don't threat tc errors as fatal, but
* try to remove as much as possible */
if (virCommandRun(cmd, &cmd_ret) < 0)
goto cleanup;
if (virNetDevBandwidthManipulateFilter(brname, NULL, id,
NULL, true, false) < 0)
goto cleanup;
virCommandFree(cmd);
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "class", "del", "dev", brname,
"classid", class_id, NULL);
if (virCommandRun(cmd, &cmd_ret) < 0)
goto cleanup;
ret = 0;
cleanup:
VIR_FREE(qdisc_id);
VIR_FREE(class_id);
virCommandFree(cmd);
return ret;
}
/**
* virNetDevBandwidthUpdateRate:
* @ifname: interface name
* @id: unique identifier
* @bandwidth: used to derive 'ceil' of class with @id
* @new_rate: new rate
*
* This function updates the 'rate' attribute of HTB class.
* It can be used whenever a new interface is plugged to a
* bridge to adjust average throughput of non guaranteed
* NICs.
*
* Returns 0 on success, -1 otherwise.
*/
int
virNetDevBandwidthUpdateRate(const char *ifname,
unsigned int id,
virNetDevBandwidthPtr bandwidth,
unsigned long long new_rate)
{
int ret = -1;
virCommandPtr cmd = NULL;
char *class_id = NULL;
char *rate = NULL;
char *ceil = NULL;
if (virAsprintf(&class_id, "1:%x", id) < 0 ||
virAsprintf(&rate, "%llukbps", new_rate) < 0 ||
virAsprintf(&ceil, "%llukbps", bandwidth->in->peak ?
bandwidth->in->peak :
bandwidth->in->average) < 0)
goto cleanup;
cmd = virCommandNew(TC);
virCommandAddArgList(cmd, "class", "change", "dev", ifname,
"classid", class_id, "htb", "rate", rate,
"ceil", ceil, NULL);
virNetDevBandwidthCmdAddOptimalQuantum(cmd, bandwidth->in);
if (virCommandRun(cmd, NULL) < 0)
goto cleanup;
ret = 0;
cleanup:
virCommandFree(cmd);
VIR_FREE(class_id);
VIR_FREE(rate);
VIR_FREE(ceil);
return ret;
}
/**
* virNetDevBandwidthUpdateFilter:
* @ifname: interface to operate on
* @ifmac_ptr: new MAC to update the filter with
* @id: filter ID
*
* Sometimes the host environment is so dynamic, that even a
* guest's MAC addresses change on the fly. When that happens we
* must update our QoS hierarchy so that the guest's traffic is
* placed into the correct QDiscs. This function updates the
* filter for the interface @ifname with the unique identifier
* @id so that it uses the new MAC address of the guest interface
* @ifmac_ptr.
*
* Returns: 0 on success,
* -1 on failure (with error reported).
*/
int
virNetDevBandwidthUpdateFilter(const char *ifname,
const virMacAddr *ifmac_ptr,
unsigned int id)
{
int ret = -1;
char *class_id = NULL;
if (virAsprintf(&class_id, "1:%x", id) < 0)
goto cleanup;
if (virNetDevBandwidthManipulateFilter(ifname, ifmac_ptr, id,
class_id, true, true) < 0)
goto cleanup;
ret = 0;
cleanup:
VIR_FREE(class_id);
return ret;
}