From bc5d7acca8855da34901ab307f5ab8c89ff42b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= Date: Thu, 28 May 2015 16:43:42 +0200 Subject: [PATCH] plugin_net: added support for adapter coalesce and features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usage: [net] coalesce=PARAM1 VALUE1 PARAM2 VALUE2 features=PARAM1 VALUE1 PARAM2 VALUE2 It is also possible to specify it by: coalesce=PARAM1: VALUE1; PARAM2: VALUE2 or: coalesce=PARAM1 VALUE1, PARAM2 VALUE2 or mix all separators. Resolves: rhbz#1152539 Resolves: rhbz#1152541 Signed-off-by: Jaroslav Škarvada --- tuned/plugins/plugin_net.py | 122 ++++++++++++++++++++++++++++++++++++ tuned/utils/commands.py | 21 ++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/tuned/plugins/plugin_net.py b/tuned/plugins/plugin_net.py index c11b99f..40ab297 100644 --- a/tuned/plugins/plugin_net.py +++ b/tuned/plugins/plugin_net.py @@ -74,11 +74,40 @@ class NetTuningPlugin(base.Plugin): log.debug("%s load: read %0.2f, write %0.2f" % (device, stats["read"], stats["write"])) log.debug("%s idle: read %d, write %d, level %d" % (device, idle["read"], idle["write"], idle["level"])) + @classmethod + def _get_config_options_coalesce(cls): + return { + "adaptive-rx": None, + "adaptive-tx": None, + "rx-usecs": None, + "rx-frames": None, + "rx-usecs-irq": None, + "rx-frames-irq": None, + "tx-usecs": None, + "tx-frames": None, + "tx-usecs-irq": None, + "tx-frames-irq": None, + "stats-block-usecs": None, + "pkt-rate-low": None, + "rx-usecs-low": None, + "rx-frames-low": None, + "tx-usecs-low": None, + "tx-frames-low": None, + "pkt-rate-high": None, + "rx-usecs-high": None, + "rx-frames-high": None, + "tx-usecs-high": None, + "tx-frames-high": None, + "sample-interval": None + } + @classmethod def _get_config_options(cls): return { "wake_on_lan": None, "nf_conntrack_hashsize": None, + "features": None, + "coalesce": None } def _init_stats_and_idle(self, instance, device): @@ -125,6 +154,41 @@ class NetTuningPlugin(base.Plugin): # speed / 7 Mb -> MB return (int) (0.6 * 1024 * 1024 * speed / 8) + # parse features/coalesce config parameters (those defined in profile configuration) + # context is for error message + def _parse_config_parameters(self, value, context): + # split supporting various dellimeters + v = str(re.sub(r"(:\s*)|(\s+)|(\s*;\s*)|(\s*,\s*)", " ", value)).split() + lv = len(v) + if lv % 2 != 0: + log.error("invalid %s parameter: '%s'" % (context, str(value))) + return None + if lv == 0: + return dict() + # convert flat list to dict + return dict(zip(v[::2], v[1::2])) + + # parse features/coalesce device parameters (those returned by ethtool) + def _parse_device_parameters(self, value): + # substitute "Adaptive RX: val1 TX: val2" to 'adaptive-rx: val1' and + # 'adaptive-tx: val2' and workaround for ethtool inconsistencies + # (rhbz#1225375) + value = self._cmd.multiple_re_replace(\ + {"Adaptive RX:": "adaptive-rx:", \ + "\s+TX:": "\nadaptive-tx:", \ + "rx-frame-low:": "rx-frames-low:", \ + "rx-frame-high:": "rx-frames-high:", \ + "tx-frame-low:": "tx-frames-low:", \ + "tx-frame-high:": "tx-frames-high:"}, value) + # remove empty lines, remove fixed parameters (those with "[fixed]") + vl = filter(lambda v: len(str(v)) > 0 and not re.search("\[fixed\]$", str(v)), value.split('\n')) + if len(vl) < 2: + return None + # skip first line (device name), split to key/value, + # remove pairs which are not key/value + return dict(filter(lambda u: len(u) == 2, \ + map(lambda v: re.split(r":\s*", str(v)), vl[1:]))) + @classmethod def _nf_conntrack_hashsize_path(self): return "/sys/module/nf_conntrack/parameters/hashsize" @@ -174,3 +238,61 @@ class NetTuningPlugin(base.Plugin): if len(value) > 0: return int(value) return None + + # d is dict: {parameter: value} + def _check_coalesce_parameters(self, d): + sck = set(d.keys()) + sck_supported = set(self._get_config_options_coalesce().keys()) + if not sck.issubset(sck_supported): + log.error("unknown coalesce parameter(s): %s" % str(sck - sck_supported)) + return False + return True + + def _get_device_parameters(self, coalesce, device): + ret, value = self._cmd.execute(["ethtool", "-c" if coalesce else "-k", device]) + if ret != 0 or len(value) == 0: + return None + d = self._parse_device_parameters(value) + if coalesce and not self._check_coalesce_parameters(d): + return None + return d + + def _set_device_parameters(self, coalesce, value, device, sim): + if value is None or len(value) == 0: + return None + d = self._parse_config_parameters(value, "coalesce" if coalesce else "features") + if d is None or (coalesce and not self._check_coalesce_parameters(d)): + return None + if not sim: + log.debug("setting %s: %s" % ("coalesce" if coalesce else "features", str(d))) + # ignore ethtool return code 80, it means parameter is already set + self._cmd.execute(["ethtool", "-C" if coalesce else "-K", device] + self._cmd.dict2list(d), [80]) + return d + + def _custom_parameters(self, coalesce, start, value, device, verify): + storage_key = self._storage_key("coalesce" if coalesce else "features", device) + if start: + cd = self._get_device_parameters(coalesce, device) + d = self._set_device_parameters(coalesce, value, device, verify) + # backup only parameters which are changed + sd = dict(filter(lambda (k, v): k in d, cd.items())) + if len(d) != len(sd): + log.error("unable to save previous %s, wanted to save: '%s', but read: '%s'" % \ + ("coalesce" if coalesce else "features", str(d.keys()), str(cd.items()))) + return False + if verify: + return self._cmd.dict2list(d) == self._cmd.dict2list(sd) + self._storage.set(storage_key," ".join(self._cmd.dict2list(sd))) + else: + if not verify: + original_value = self._storage.get(storage_key) + self._set_device_parameters(coalesce, original_value, device, False) + return None + + @command_custom("features", per_device = True) + def _features(self, start, value, device, verify): + return self._custom_parameters(False, start, value, device, verify) + + @command_custom("coalesce", per_device = True) + def _coalesce(self, start, value, device, verify): + return self._custom_parameters(True, start, value, device, verify) diff --git a/tuned/utils/commands.py b/tuned/utils/commands.py index ab3bef6..86ae60f 100644 --- a/tuned/utils/commands.py +++ b/tuned/utils/commands.py @@ -29,6 +29,22 @@ class commands: def remove_ws(self, s): return re.sub('\s+', ' ', s).strip() + # convert dictionary 'd' to flat list and return it + # it uses sort on the dictionary items to return consistent results + # for directories with different inserte/delete history + def dict2list(self, d): + l = [] + if d is not None: + for i in sorted(d.items()): + l += list(i) + return l + + # Do multiple regex replaces in 's' according to lookup table described by + # dictionary 'd', e.g.: d = {"re1": "replace1", "re2": "replace2"} + def multiple_re_replace(self, d, s): + r = re.compile("(%s)" % ")|(".join(d.keys())) + return r.sub(lambda mo: d.values()[mo.lastindex - 1], s) + def write_to_file(self, f, data): self._debug("Writing to file: %s < %s" % (f, data)) try: @@ -57,7 +73,8 @@ class commands: return False; return self.write_to_file(f, re.sub(pattern, repl, data, flags = re.MULTILINE)) - def execute(self, args): + # "no_errors" can be list of return codes not treated as errors + def execute(self, args, no_errors = []): retcode = None if self._environment is None: self._environment = os.environ.copy() @@ -70,7 +87,7 @@ class commands: out, err = proc.communicate() retcode = proc.returncode - if retcode: + if retcode and not retcode in no_errors: err_out = err[:-1] if len(err_out) == 0: err_out = out[:-1]