[RFC,v2,9/9] usertools/lib: add GRUB utility library for hugepage config
Checks
Commit Message
This library is highly experimental and can kill kittens, but its
main purpose is to automatically set up GRUB command-line to
allocate a given number of hugepages at boot time. It works in
a similar way HugeUtil library does, but instead of committing
changes to fstab or runtime configuration, it commits its
changes to GRUB default command-line and updates all GRUB entries
afterwards. I got it to a state where it's safe to use on my
system, but see the part above about killing kittens - you have
been warned :)
No example scripts will currently be provided.
Signed-off-by: Anatoly Burakov <anatoly.burakov@intel.com>
---
usertools/DPDKConfigLib/GrubHugeUtil.py | 175 ++++++++++++++++++++++++
1 file changed, 175 insertions(+)
create mode 100755 usertools/DPDKConfigLib/GrubHugeUtil.py
new file mode 100755
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2018 Intel Corporation
+
+
+from .PlatformInfo import *
+from .Util import *
+import re
+import os
+
+__KERNEL_TRANSPARENT_HP = "/sys/kernel/mm/transparent_hugepage/enabled"
+_GRUB_CMDLINE_PARAM_NAME = "GRUB_CMDLINE_LINUX_DEFAULT"
+
+# local copy of platform info
+info = PlatformInfo()
+
+def _find_linux_default_cmdline():
+ with open("/etc/default/grub") as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith(_GRUB_CMDLINE_PARAM_NAME):
+ return line
+ else:
+ raise RuntimeError("Invalid GRUB default configuration format")
+
+
+def _parse_linux_default_cmdline(line):
+ # get value to the right of equals sign, strip whitespace and quotes,
+ # split into separate keys and make a list of values
+ _, cmdline = kv_split(line, "=")
+ # remove quotes
+ if cmdline[0] == cmdline[-1] == '"':
+ cmdline = cmdline[1:-1]
+
+ return [kv_split(v, "=") for v in cmdline.split()]
+
+
+def _generate_linux_default_cmdline(cmdline):
+ lines = []
+ cmdline_idx = -1
+ with open("/etc/default/grub") as f:
+ for idx, line in enumerate(f):
+ line = line.strip()
+ lines.extend([line])
+ if line.startswith(_GRUB_CMDLINE_PARAM_NAME):
+ cmdline_idx = idx
+ if cmdline_idx == -1:
+ raise RuntimeError("Invalid GRUB default configuration format")
+
+ # write the lines back, replacing one we want
+ with open("/etc/default/grub", "w") as f:
+ for idx, line in enumerate(lines):
+ if idx == cmdline_idx:
+ line = cmdline
+ f.write(line + "\n")
+
+
+def _find_transparent_hugepage():
+ if not os.path.exists(__KERNEL_TRANSPARENT_HP):
+ return None
+ value = read_file(__KERNEL_TRANSPARENT_HP)
+ m = re.search(r"\[([a-z]+)\]", value)
+ if not m:
+ raise RuntimeError("BUG: Bad regular expression")
+ return m.group(1)
+
+
+class GrubHugepageConfig:
+ def __init__(self):
+ self.update()
+
+ def update(self):
+ self.reset()
+
+ hugepage_sizes = info.hugepage_sizes_supported
+ if len(hugepage_sizes) == 0:
+ raise RuntimeError("Hugepages appear to be unsupported")
+ cmdline = _find_linux_default_cmdline()
+ values = _parse_linux_default_cmdline(cmdline)
+
+ # parse values in the list
+ self.default_hugepagesz = info.default_hugepage_size
+ self.transparent_hugepage = _find_transparent_hugepage()
+ sizes = []
+ nrs = []
+ for k, v in values:
+ if k == "default_hugepagesz":
+ self.default_hugepagesz = human_readable_to_kilobytes(v)
+ elif k == "transparent_hugepage":
+ self.transparent_hugepage = v
+ elif k == "hugepagesz":
+ sizes.append(human_readable_to_kilobytes(v))
+ elif k == "hugepages":
+ nrs.append(v)
+ if len(sizes) != len(nrs):
+ raise RuntimeError("GRUB hugepage configuration is wrong")
+ detected_hugepages = dict(zip(sizes, map(int, nrs)))
+ self.nr_hugepages = {size: detected_hugepages.get(size, 0)
+ for size in hugepage_sizes}
+
+ def commit(self):
+ # perform sanity checks - we can't afford invalid data making it into
+ # bootloader config, as that might render user's machine unbootable, so
+ # tread really really carefully
+
+ # first, check if user didn't add any unexpected hugepage sizes
+ configured_sizes = set(self.nr_hugepages.keys())
+ supported_sizes = set(info.hugepage_sizes_supported)
+
+ if configured_sizes != supported_sizes:
+ diff = configured_sizes.difference(supported_sizes)
+ raise ValueError("Unsupported hugepage sizes: %s" %
+ [kilobytes_to_human_readable(s) for s in diff])
+
+ # check if default hugepage is one of the supported ones
+ if self.default_hugepagesz is not None and\
+ self.default_hugepagesz not in configured_sizes:
+ s = kilobytes_to_human_readable(self.default_hugepagesz)
+ raise ValueError("Unsupported default hugepage size: %i" % s)
+
+ # transparent hugepages support was added in recent kernels, so check
+ # if user is trying to set this
+ if _find_transparent_hugepage() is None and \
+ self.transparent_hugepage is not None:
+ raise ValueError("Transparent hugepages are not unsupported")
+
+ # OK, parameters look to be valid - let's roll
+
+ # read and parse current cmdline
+ cmdline = _find_linux_default_cmdline()
+
+ values = _parse_linux_default_cmdline(cmdline)
+
+ # clear out old data
+ klist = ["transparent_hugepage", "default_hugepagesz",
+ "hugepage", "hugepagesz"]
+ # iterate over a copy so that we could delete items
+ for k, v in values[:]:
+ if k in klist:
+ values.remove((k, v))
+
+ # generate new cmdline
+ cmdline = " ".join([("%s=%s" % (k, v)) if v is not None else k
+ for k, v in values])
+
+ # now, populate cmdline with new data
+ new_items = []
+ for sz, nr in self.nr_hugepages.items():
+ sz = kilobytes_to_human_readable(sz)
+ new_items += "hugepagesz=%s hugepages=%i" % (sz, nr)
+ if self.default_hugepagesz is not None:
+ new_items += "default_hugepagesz=%i" % self.default_hugepagesz
+ if self.transparent_hugepage is not None:
+ new_items += "transparent_hugepage=%s" % self.transparent_hugepage
+
+ cmdline = "%s %s" % (cmdline, " ".join(new_items))
+
+ # strip any extraneous whitespace we may have added
+ cmdline = re.sub(r"\s\s+", " ", cmdline).strip()
+
+ # now, put everything back together
+ cmdline = '%s="%s"' % (_GRUB_CMDLINE_PARAM_NAME, cmdline)
+
+ # write it to config
+ _generate_linux_default_cmdline(cmdline)
+
+ # finally, update GRUB
+ if not run(["update-grub"]):
+ raise RuntimeError("Failed to update GRUB")
+ self.update()
+
+ def reset(self):
+ self.nr_hugepages = {} # pagesz: number
+ self.default_hugepagesz = None
+ self.transparent_hugepage = None