From 3b9ae1533709433f0abdff304e79e2871794b4b8 Mon Sep 17 00:00:00 2001 From: Rich Megginson Date: Wed, 7 Aug 2024 10:32:57 -0600 Subject: [PATCH] fix: Use tuned files instead of using it as a module The previous version of the kernel_settings role used `tuned` as a python library, and had a `kernel_settings` module which was a wrapper around this code. However, `tuned` version 2.23 has changed its internal API and it is no longer possible to use it as a python library. Instead, the kernel_settings role has been refactored to read/write `tuned` config files, and let the `tuned` daemon manage the settings. In addition, `tuned` 2.23 changed the location of the profile directory, so the kernel_settings role will now determine the location of the profile directory depending on the `tuned` version. The old `kernel_settings` module is removed, along with all of the python unit testing code. A new `kernel_settings_get_config` module has been created which will simply parse and return the given config file as a `dict`. Signed-off-by: Rich Megginson --- .github/config/ubuntu-requirements.txt | 6 - .sanity-ansible-ignore-2.10.txt | 2 +- .sanity-ansible-ignore-2.11.txt | 2 +- .sanity-ansible-ignore-2.12.txt | 2 +- .sanity-ansible-ignore-2.13.txt | 2 +- .sanity-ansible-ignore-2.14.txt | 2 +- .sanity-ansible-ignore-2.15.txt | 2 +- .sanity-ansible-ignore-2.16.txt | 2 +- .sanity-ansible-ignore-2.17.txt | 2 +- .sanity-ansible-ignore-2.9.txt | 2 +- ansible_pytest_extra_requirements.txt | 3 - contributing.md | 115 --- library/kernel_settings.py | 937 ------------------ library/kernel_settings_get_config.py | 93 ++ pytest_extra_requirements.txt | 4 +- tasks/main.yml | 237 +++-- templates/kernel_settings.j2 | 31 +- tests/install_ansible_for_testing.sh | 27 - tests/install_tuned_for_testing.sh | 60 -- tests/roles/caller/tasks/main.yml | 1 + tests/run_ansible_for_testing.sh | 7 - tests/tasks/cleanup.yml | 72 ++ tests/tests_bool_not_allowed.yml | 30 +- tests/tests_change_settings.yml | 384 +++---- tests/tests_default.yml | 15 +- tests/tests_include_vars_from_parent.yml | 99 +- tests/tests_simple_settings.yml | 120 ++- .../tuned/etc/tuned/basic_settings/tuned.conf | 24 - tests/tuned/etc/tuned/bogus/tuned.conf | 2 - .../etc/tuned/empty_profile_file/tuned.conf | 0 .../etc/tuned/kernel_settings/tuned.conf | 5 - tests/tuned/etc/tuned/tuned-main.conf | 42 - tests/unit/__init__.py | 0 tests/unit/modules/__init__.py | 0 tests/unit/modules/test_kernel_settings.py | 586 ----------- tox.ini | 9 - tuned_requirements.txt | 13 - vars/main.yml | 16 +- 38 files changed, 739 insertions(+), 2217 deletions(-) delete mode 100644 .github/config/ubuntu-requirements.txt delete mode 100644 library/kernel_settings.py create mode 100644 library/kernel_settings_get_config.py delete mode 100755 tests/install_ansible_for_testing.sh delete mode 100755 tests/install_tuned_for_testing.sh delete mode 100755 tests/run_ansible_for_testing.sh create mode 100644 tests/tasks/cleanup.yml delete mode 100644 tests/tuned/etc/tuned/basic_settings/tuned.conf delete mode 100644 tests/tuned/etc/tuned/bogus/tuned.conf delete mode 100644 tests/tuned/etc/tuned/empty_profile_file/tuned.conf delete mode 100644 tests/tuned/etc/tuned/kernel_settings/tuned.conf delete mode 100644 tests/tuned/etc/tuned/tuned-main.conf delete mode 100644 tests/unit/__init__.py delete mode 100644 tests/unit/modules/__init__.py delete mode 100644 tests/unit/modules/test_kernel_settings.py delete mode 100644 tuned_requirements.txt diff --git a/.github/config/ubuntu-requirements.txt b/.github/config/ubuntu-requirements.txt deleted file mode 100644 index 48bf96f..0000000 --- a/.github/config/ubuntu-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -libdbus-1-dev -libgirepository1.0-dev -python3-dev -libssl-dev -libcairo2-dev -python2-dev diff --git a/.sanity-ansible-ignore-2.10.txt b/.sanity-ansible-ignore-2.10.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.10.txt +++ b/.sanity-ansible-ignore-2.10.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.11.txt b/.sanity-ansible-ignore-2.11.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.11.txt +++ b/.sanity-ansible-ignore-2.11.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.12.txt b/.sanity-ansible-ignore-2.12.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.12.txt +++ b/.sanity-ansible-ignore-2.12.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.13.txt b/.sanity-ansible-ignore-2.13.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.13.txt +++ b/.sanity-ansible-ignore-2.13.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.14.txt b/.sanity-ansible-ignore-2.14.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.14.txt +++ b/.sanity-ansible-ignore-2.14.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.15.txt b/.sanity-ansible-ignore-2.15.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.15.txt +++ b/.sanity-ansible-ignore-2.15.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.16.txt b/.sanity-ansible-ignore-2.16.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.16.txt +++ b/.sanity-ansible-ignore-2.16.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.17.txt b/.sanity-ansible-ignore-2.17.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.17.txt +++ b/.sanity-ansible-ignore-2.17.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/.sanity-ansible-ignore-2.9.txt b/.sanity-ansible-ignore-2.9.txt index 43e2048..49d8598 100644 --- a/.sanity-ansible-ignore-2.9.txt +++ b/.sanity-ansible-ignore-2.9.txt @@ -1 +1 @@ -plugins/modules/kernel_settings.py validate-modules:missing-gplv3-license +plugins/modules/kernel_settings_get_config.py validate-modules:missing-gplv3-license diff --git a/ansible_pytest_extra_requirements.txt b/ansible_pytest_extra_requirements.txt index 6bafb6f..c5eee1e 100644 --- a/ansible_pytest_extra_requirements.txt +++ b/ansible_pytest_extra_requirements.txt @@ -1,6 +1,3 @@ # SPDX-License-Identifier: MIT # ansible and dependencies for all supported platforms -ansible ; python_version > "2.6" -idna<2.8 ; python_version < "2.7" -PyYAML<5.1 ; python_version < "2.7" diff --git a/contributing.md b/contributing.md index d8777fd..e3a8940 100644 --- a/contributing.md +++ b/contributing.md @@ -19,118 +19,3 @@ are likely to be suitable for new contributors! **Code** is managed on [Github](https://github.com/linux-system-roles/kernel_settings), using [Pull Requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). - -## Python Code - -The Python code needs to be **compatible with the Python versions supported by -the role platform**. - -For example, see [meta](https://github.com/linux-system-roles/kernel_settings/blob/main/meta/main.yml) -for the platforms supported by the role. - -If the role provides Ansible modules (code in `library/` or `module_utils/`) - -these run on the *managed* node, and typically[1] use the default system python: - -* EL6 - python 2.6 -* EL7 - python 2.7 or python 3.6 in some cases -* EL8 - python 3.6 -* EL9 - python 3.9 - -If the role provides some other sort of Ansible plugin such as a filter, test, -etc. - these run on the *control* node and typically use whatever version of -python that Ansible uses, which in many cases is *not* the system python, and -may be a modularity release such as python311. - -In general, it is a good idea to ensure the role python code works on all -versions of python supported by `tox-lsr` from py36 on, and on py27 if the role -supports EL7, and on py26 if the role supports EL6.[1] - -[1] Advanced users may set -[ansible_python_interpreter](https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html#term-ansible_python_interpreter) -to use a non-system python on the managed node, so it is a good idea to ensure -your code has broad python version compatibility, and do not assume your code -will only ever be run with the default system python. - -## Testing kernel_settings modules - -It is recommended to use `tox` to set up your `virtualenv` for -development/testing purposes: - -```bash -dnf/yum install python-tox -tox -e py38 -``` - -You can also use the virtualenv created by `tox` just like any -other virtualenv created by `python-virtualenv`: - -```bash -. .tox/env-py38/bin/activate -python ->>> import package.that.only.exists.in.venv -``` - -The unit tests and other tests are run by default when you use `tox` by itself -or `tox -e py38` for a specific python versioned environment. Note that other -operating system packages may be required to be installed in order for `tox` -to use `pip` to install python dependencies e.g. for python packages which -have native components. - -I would also strongly encourage you to use an IDE for development. For example, -Visual Studio code python extension auto-discovers tests and allows you to -run and debug unit tests. However, you may need to create a `.env` file like -this, in order for code navigation, auto-completion, and test discovery to -work correctly: - -```bash -PYTHONPATH=/full/path/to/tuned:/full/path/to/linux-system-roles/kernel_settings/library -``` - -### Testing the module - -[Ansible Module Development Guide](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html) - -Using a tox python virtualenv from the `kernel_settings` directory: - -```bash -. .tox/env-py38/bin/activate -TESTING=true [TEST_PROFILE=kernel_settings] python \ - library/kernel_settings.py args.json -``` - -looks for test profiles under `tests/tuned/etc/tuned` - -to run the code in the debugger: - -```bash -TESTING=true [TEST_PROFILE=kernel_settings] python -mpdb \ - library/kernel_settings.py args.json -``` - -Where `args.json` looks like this: - -```json -{ - "ANSIBLE_MODULE_ARGS": { - "name": "kernel_settings", - "sysctl": [ - {"name": "fs.inotify.max_user_watches", "value": 524288}, - {"name": "kernel.threads-max", "value": 30001} - ], - "sysfs": [ - {"name": "/sys/kernel/kexec_crash_size", "value": 337641472} - ], - "bootloader": [ - {"name": "cmdline", "value": [ - {"name": "mitigations", "value": "on"}, - {"name": "another"} - ] - } - ], - "selinux": [ - {"name": "avc_cache_threshold", "value": 512} - ], - "purge": false - } -} -``` diff --git a/library/kernel_settings.py b/library/kernel_settings.py deleted file mode 100644 index 8e47e71..0000000 --- a/library/kernel_settings.py +++ /dev/null @@ -1,937 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, Rich Megginson -# SPDX-License-Identifier: GPL-2.0-or-later -# -""" Manage kernel settings using tuned via a wrapper """ - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = { - "metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community", -} - -DOCUMENTATION = """ ---- -module: kernel_settings - -short_description: Manage kernel settings using tuned via a wrapper - -version_added: "2.13.0" - -description: - - "WARNING: Do not use this module directly! It is only for role internal use." - - | - Manage kernel settings using tuned via a wrapper. The options correspond - to names of units or plugins in tuned. For example, the option C(sysctl) - corresponds to the C(sysctl) unit or plugin in tuned. Setting parameters - works mostly like it does with tuned, except that this module uses Ansible - YAML format instead of the tuned INI-style profile file format. This module - creates a special tuned profile C(kernel_settings) which will be applied by - tuned before any other profiles, allowing the user to configure tuned to - override settings made by this module. You should be aware of this if you - plan to use tuned in addition to using this module. - - HORIZONTALLINE - - | - NOTE: the options list may be incomplete - the actual options are generated - dynamically from tuned, for the current options supported by the version - of tuned, which are the tuned supported plugins. Only the most common - options are listed. See the tuned documentation for the full list and - more information. - - HORIZONTALLINE - - | - Each option takes a list or dict of settings. Each setting is a C(dict). - The C(dict) must have one of the keys C(name), C(value), C(state), or C(previous). - C(state) is used to remove settings or sections of settings. C(previous) is - used to replace all of values in a section with the given values. The only case - where an option takes a dict is when you want to remove a section completely - - then value for the section is the dict C({"state":"empty"}). - If you specify multiple settings with the same name in a section, the last one - will be used. -options: - sysctl: - description: - - | - list of sysctl settings to apply - this works mostly - like the C(sysctl) module except that C(/etc/sysctl.conf) and files - under C(/etc/sysctl.d) are not used. - required: false - type: raw - sysfs: - description: - - key/value pairs of sysfs settings to apply - required: false - type: raw - modules: - description: - - key/value pairs of module settings to apply - required: false - type: raw - selinux: - description: - - key/value pairs of selinux settings to apply - required: false - type: raw - systemd: - description: - - key/value pairs of systemd settings to apply - required: false - type: raw - vm: - description: - - key/value pairs of vm settings to apply - required: false - type: raw - bootloader: - description: - - the C(cmdline) option can be used to set, add, or delete - kernel command line options. See EXAMPLES for some examples - of how to use this option. - Note that this uses the tuned implementation, which adds these - options to whatever the default bootloader command line arguments - tuned is historically used to add/delete performance related - kernel command line arguments e.g. C(spectre_v2=off). If you need - more general purpose bootloader configuration, you should use - a bootloader module/role. - type: raw - purge: - description: - - Remove the current kernel_settings, whatever they are, and force - the given settings to be the current and only settings - type: bool - default: false - ansible_managed_new: - description: - - Ansible ansible_managed string to put in header of file - - should be in the format of {{ ansible_managed | comment }} - - as rendered by the template module - type: str - required: true - ansible_managed_current: - description: - - the current config file header to be compared and replaced - type: str - required: true - -author: - - Rich Megginson (@richm) -""" - -EXAMPLES = """ -# Add or replace the sysctl `fs.file-max` parameter with the value 65535. The -# existing settings are not touched. -- name: Add or replace the sysctl `fs.file-max` parameter with the value 65535. - kernel_settings: - sysctl: - - name: fs.file-max - value: 65535 - -- name: remove the entire sysctl section - kernel_settings: - sysctl: - state: empty - -- name: remove the entire sysctl section and replace with the given values - kernel_settings: - sysctl: - - previous: replaced - - name: fs.file-max - value: 65535 - -- name: add the sysctl vm.max_mmap_regions, and disable spectre/meltdown security - kernel_settings: - sysctl: - - name: vm.max_mmap_regions - value: 262144 - sysfs: - - name: /sys/kernel/debug/x86/pti_enabled - value: 0 - - name: /sys/kernel/debug/x86/retp_enabled - value: 0 - - name: /sys/kernel/debug/x86/ibrs_enabled - value: 0 - -# replace the existing sysctl section with the specified section -# delete the /sys/kernel/debug/x86/retp_enabled setting -# completely remove the vm section -# add the bootloader cmdline arguments spectre_v2=off nopti -# remove the bootloader cmdline arguments panic splash -- name: more settings - kernel_settings: - sysctl: - - previous: replaced - - name: vm.max_mmap_regions - value: 262144 - sysfs: - - name: /sys/kernel/debug/x86/retp_enabled - state: absent - vm: - state: empty - bootloader: - - name: cmdline - value: - - name: spectre_v2 - value: "off" - - name: nopti - - name: panic - state: absent - - name: splash - state: absent -""" - -RETURN = """ -msg: - description: | - A short text message to say what action this module performed. - returned: always - type: str -new_profile: - description: | - This is the tuned profile in dict format, after the changes, if any, - have been applied. - returned: always - type: dict -reboot_required: - description: | - default C(false) - if true, this means a reboot of the managed host is - required in order for the changes to take effect. - returned: always - type: bool -active_profile: - description: | - This is the space delimited list of active profiles as reported - by tuned. - returned: always - type: str -""" - -import os -import logging -import re -import tempfile -import shutil -import shlex -import contextlib -import atexit # for testing -import copy - -try: - import configobj - - HAS_CONFIGOBJ = True -except ImportError: - HAS_CONFIGOBJ = False -import ansible.module_utils.six as ansible_six - -# This is a bit of a mystery - bug in pylint? -# pylint: disable=import-error -import ansible.module_utils.six.moves as ansible_six_moves - -from ansible.module_utils.basic import AnsibleModule - -# see https://github.com/python/cpython/blob/main/Lib/logging/__init__.py -# for information about logging module internals - - -class TunedLogWrapper(logging.getLoggerClass()): - """This wraps the tuned logger so that we can intercept logs and handle them here""" - - def __init__(self, *args, **kwargs): - super(TunedLogWrapper, self).__init__(*args, **kwargs) - self.setLevel(logging.DEBUG) - self.logstack = [] - - def handle(self, record): - self.logstack.append(record) - - -@contextlib.contextmanager -def save_and_restore_logging_methods(): - """do not allow tuned logging to pollute global logging module or - ansible logging""" - save_logging_add_level_name = logging.addLevelName - - def wrapper_add_level_name(_levelval, _levelname): - """ignore tuned.logging call to logging.addLevelName""" - # print('addLevelName wrapper ignoring {} {}'.format(levelval, levelname)) - - logging.addLevelName = wrapper_add_level_name - save_logging_set_logger_class = logging.setLoggerClass - - def wrapper_set_logger_class(_clsname): - """ignore tuned.logging call to logging.setLoggerClass""" - # print('setLoggerClass wrapper ignoring {}'.format(clsname)) - - logging.setLoggerClass = wrapper_set_logger_class - try: - yield - finally: - logging.addLevelName = save_logging_add_level_name - logging.setLoggerClass = save_logging_set_logger_class - - -caught_import_error = None -HAS_TUNED = False -try: - with save_and_restore_logging_methods(): - import tuned.logs - HAS_TUNED = True -except ImportError as ierr: - # tuned package might not be available in check mode - so just - # note that this is missing, and do not report in check mode - caught_import_error = ierr - -if HAS_TUNED: - tuned.logs.root_logger = TunedLogWrapper(__name__) - tuned.logs.get = lambda: tuned.logs.root_logger - - import tuned.consts - import tuned.utils.global_config - import tuned.daemon - from tuned.exceptions import TunedException - - -TUNED_PROFILE = os.environ.get("TEST_PROFILE", "kernel_settings") -NOCHANGES = 0 -CHANGES = 1 -REMOVE_SECTION_VALUE = {"state": "empty"} -SECTION_TO_REPLACE = "__section_to_replace" -ERR_SECTION_MISSING_NAME = "Error: section [{0}] item is missing 'name': {1}" -ERR_NAME_NOT_VALID = "Error: section [{0}] item name [{1}] is not a valid string" -ERR_NO_VALUE_OR_STATE = ( - "Error: section [{0}] item name [{1}] must have either a 'value' or 'state'" -) -ERR_BOTH_VALUE_AND_STATE = ( - "Error: section [{0}] item name [{1}] must have only one of 'value' or 'state'" -) -ERR_STATE_ABSENT = ( - "Error: section [{0}] item name [{1}] state value must be 'absent' not [{2}]" -) -ERR_UNEXPECTED_VALUES = "Error: section [{0}] item [{1}] has unexpected values {2}" -ERR_VALUE_ITEM_NOT_DICT = ( - "Error: section [{0}] item name [{1}] value [{2}] is not a dict" -) -ERR_VALUE_ITEM_PREVIOUS = ( - "Error: section [{0}] item name [{1}] has invalid value for 'previous' [{2}]" -) -ERR_REMOVE_SECTION_VALUE = "Error: to remove the section [{0}] specify the value {1}" -ERR_ITEM_NOT_DICT = "Error: section [{0}] item value [{1}] is not a dict" -ERR_ITEM_PREVIOUS = "Error: section [{0}] item has invalid value for 'previous' [{1}]" -ERR_ITEM_DICT_OR_LIST = "Error: section [{0}] value must be a dict or a list" -ERR_LIST_NOT_ALLOWED = "Error: section [{0}] item [{1}] has unexpected list value {2}" -ERR_BLCMD_MUST_BE_LIST = "Error: section [{0}] item [{1}] must be a list not [{2}]" -ERR_VALUE_CANNOT_BE_BOOLEAN = ( - "Error: section [{0}] item [{1}] value [{2}] must not " - "be a boolean - try quoting the value" -) - - -def get_supported_tuned_plugin_names(): - """get names of all tuned plugins supported by this module""" - return [ - "bootloader", - "modules", - "selinux", - "sysctl", - "sysfs", - "systemd", - "vm", - ] - - -def no_such_profile(): - """see if the last log message was that the profile did not exist""" - lastlogmsg = tuned.logs.root_logger.logstack[-1].msg - return re.search("Requested profile .* doesn't exist", lastlogmsg) is not None - - -def profile_to_dict(profile): - """convert profile object to dict""" - ret_val = {} - for unitname, unit in profile.units.items(): - ret_val[unitname] = unit.options - return ret_val - - -def debug_print_profile(profile, amodule): - """for debugging - print profile as INI""" - amodule.debug("profile {0}".format(profile.name)) - amodule.debug(str(profile_to_dict(profile))) - - -caught_name_error = None -try: - EMPTYUNIT = tuned.profiles.unit.Unit("empty", {}) -except NameError as nerr: - # tuned not loaded in check mode - caught_name_error = nerr - - -def get_profile_unit_key(profile, unitname, key): - """convenience function""" - return profile.units.get(unitname, EMPTYUNIT).options.get(key) - - -class BLCmdLine(object): - """A data type for handling bootloader cmdline values.""" - - def __init__(self, val): - self.key_list = [] # list of keys in order - to preserve ordering - self.key_to_val = {} # maps key to value - if val: - for item in shlex.split(val): - key, val = self.splititem(item) - self.key_list.append(key) - # None or '' means bare key - no value - self.key_to_val[key] = val - - @classmethod - def splititem(cls, item): - """split item in form key=somevalue into key and somevalue""" - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - key, _, val = item.partition("=") - return key, val - - @classmethod - def escapeval(cls, val): - """make sure val is quoted as in shell""" - return ansible_six_moves.shlex_quote(str(val)) - - def __str__(self): - vallist = [] - for key in self.key_list: - val = self.key_to_val[key] - if val: - vallist.append("{0}={1}".format(key, self.escapeval(val))) - else: - vallist.append(key) - return " ".join(vallist) - - def add(self, key, val): - """add/replace the given key & value""" - if key not in self.key_to_val: - self.key_list.append(key) - self.key_to_val[key] = val - - def remove(self, key): - """remove the given key""" - if key in self.key_to_val: - self.key_list.remove(key) - del self.key_to_val[key] - - -def apply_bootloader_cmdline_item(item, unitname, current_profile, curvalue): - """apply a bootloader cmdline item to the current profile""" - name = item["name"] - if item.get(SECTION_TO_REPLACE, False): - blcmd = BLCmdLine("") - else: - blcmd = BLCmdLine(curvalue) - for subitem in item["value"]: - if "previous" in subitem: - continue - if subitem.get("state") == "absent": - blcmd.remove(subitem["name"]) - else: - blcmd.add(subitem["name"], subitem.get("value")) - blcmdstr = str(blcmd) - if blcmdstr: - current_profile.units.setdefault( - unitname, tuned.profiles.unit.Unit(unitname, {}) - ).options[name] = blcmdstr - elif curvalue: - del current_profile.units[unitname].options[name] - - -def apply_item_to_profile(item, unitname, current_profile): - """apply a specific item from a section to the current_profile""" - name = item["name"] - curvalue = get_profile_unit_key(current_profile, unitname, name) - newvalue = item.get("value") - if item.get("state", None) == "absent": - if curvalue: - del current_profile.units[unitname].options[name] - elif unitname == "bootloader" and name == "cmdline": - apply_bootloader_cmdline_item(item, unitname, current_profile, curvalue) - else: - current_profile.units.setdefault( - unitname, tuned.profiles.unit.Unit(unitname, {}) - ).options[name] = str(newvalue) - - -def is_reboot_required(unitname): - """Some changes need a reboot for the changes to be applied - For example, bootloader cmdline changes""" - # for now, only bootloader cmdline changes need a reboot - return unitname == "bootloader" - - -def apply_params_to_profile(params, current_profile, purge): - """apply the settings from the input parameters to the current profile - delete the unit if it is empty after applying the parameter deletions - """ - changestatus = NOCHANGES - reboot_required = False - section_to_replace = params.pop(SECTION_TO_REPLACE, {}) - need_purge = set() - if purge: - # remove units not specified in params - need_purge = set(current_profile.units.keys()) - for unitname, items in params.items(): - unit = current_profile.units.get(unitname, None) - current_options = {} - if unit: - current_options = copy.deepcopy(unit.options) - replace = section_to_replace.get(unitname, purge) - if replace or (items == REMOVE_SECTION_VALUE): - if unit: - unit.options.clear() - if purge and unitname in need_purge: - need_purge.remove(unitname) - if items == REMOVE_SECTION_VALUE: - if unit: - # we changed the profile - changestatus = CHANGES - reboot_required = reboot_required or is_reboot_required(unitname) - # we're done - no further processing necessary for this unit - continue - for item in items: - if item and "previous" not in item: - apply_item_to_profile(item, unitname, current_profile) - newoptions = {} - if unitname in current_profile.units: - newoptions = current_profile.units[unitname].options - if current_options != newoptions: - changestatus = CHANGES - reboot_required = reboot_required or is_reboot_required(unitname) - for unitname in need_purge: - del current_profile.units[unitname] - changestatus = CHANGES - reboot_required = reboot_required or is_reboot_required(unitname) - # remove empty units - for unitname in list(current_profile.units.keys()): - if not current_profile.units[unitname].options: - del current_profile.units[unitname] - return changestatus, reboot_required - - -def write_profile(current_profile, ansible_managed_new): - """write the profile to the profile file""" - # convert profile to configobj to write ini-style file - # profile.options go into [main] section - # profile.units go into [unitname] section - cfg = configobj.ConfigObj(indent_type="") - # ansible_managed_new should be in the format of - # ansible_managed_new passed through the comment filter - # as rendered by the ansible "template" module - strip the - # trailing newline, if any - cfg.initial_comment = ansible_managed_new.strip().split("\n") - cfg["main"] = current_profile.options - for unitname, unit in current_profile.units.items(): - cfg[unitname] = unit.options - profile_base_dir = tuned.consts.LOAD_DIRECTORIES[-1] - prof_fname = os.path.join( - profile_base_dir, TUNED_PROFILE, tuned.consts.PROFILE_FILE - ) - with open(prof_fname, "wb") as prof_f: - cfg.write(prof_f) - - -def update_current_profile_and_mode(daemon, profile_list): - """ensure that the tuned current_profile applies the kernel_settings last""" - changed = False - profile, manual = daemon._get_startup_profile() - # is TUNED_PROFILE in the list? - profile_list.extend(profile.split()) - if TUNED_PROFILE not in profile_list: - changed = True - profile_list.append(TUNED_PROFILE) - # have to convert to manual mode in order to ensure kernel_settings are applied - if not manual: - changed = True - manual = True - if changed: - daemon._save_active_profile(" ".join(profile_list), manual) - return changed - - -def setup_for_testing(): - """create an /etc/tuned and /usr/lib/tuned directory structure for testing""" - test_root_dir = os.environ.get("TEST_ROOT_DIR") - if test_root_dir is None: - test_root_dir = tempfile.mkdtemp(suffix=".lsr") - # copy all of the test configs and profiles - test_root_dir_tuned = os.path.join(test_root_dir, "tuned") - test_src_dir = os.environ.get("TEST_SRC_DIR", "tests") - src_dir = os.path.join(test_src_dir, "tuned") - shutil.copytree(src_dir, test_root_dir_tuned) - # patch all of the consts to use the test_root_dir - orig_consts = {} - for cnst in ( - "GLOBAL_CONFIG_FILE", - "ACTIVE_PROFILE_FILE", - "PROFILE_MODE_FILE", - "RECOMMEND_CONF_FILE", - "BOOT_CMDLINE_FILE", - ): - orig_consts[cnst] = tuned.consts.__dict__[cnst] - fname = os.path.join( - test_root_dir_tuned, - os.path.relpath(tuned.consts.__dict__[cnst], os.path.sep), - ) - tuned.consts.__dict__[cnst] = fname - dname = os.path.dirname(fname) - if not os.path.isdir(dname): - os.makedirs(dname) - orig_load_dirs = [] - for idx, dname in enumerate(tuned.consts.LOAD_DIRECTORIES): - orig_load_dirs.append(dname) - newdname = os.path.join( - test_root_dir_tuned, os.path.relpath(dname, os.path.sep) - ) - tuned.consts.LOAD_DIRECTORIES[idx] = newdname - if not os.path.isdir(newdname): - os.makedirs(newdname) - orig_rec_dirs = [] - for idx, dname in enumerate(tuned.consts.RECOMMEND_DIRECTORIES): - orig_rec_dirs.append(dname) - newdname = os.path.join( - test_root_dir_tuned, os.path.relpath(dname, os.path.sep) - ) - tuned.consts.RECOMMEND_DIRECTORIES[idx] = newdname - if not os.path.isdir(newdname): - os.makedirs(newdname) - has_func = bool( - getattr(tuned.utils.global_config.GlobalConfig.__init__, "__func__", False) - ) - if has_func: - orig_gc_init_defaults = ( - tuned.utils.global_config.GlobalConfig.__init__.__func__.__defaults__ - ) - orig_gc_load_config_defaults = ( - tuned.utils.global_config.GlobalConfig.load_config.__func__.__defaults__ - ) - if orig_gc_init_defaults: - tuned.utils.global_config.GlobalConfig.__init__.__func__.__defaults__ = ( - tuned.consts.GLOBAL_CONFIG_FILE, - ) - if orig_gc_load_config_defaults: - tuned.utils.global_config.GlobalConfig.load_config.__func__.__defaults__ = ( - tuned.consts.GLOBAL_CONFIG_FILE, - ) - else: - orig_gc_init_defaults = ( - tuned.utils.global_config.GlobalConfig.__init__.__defaults__ - ) - orig_gc_load_config_defaults = ( - tuned.utils.global_config.GlobalConfig.load_config.__defaults__ - ) - if orig_gc_init_defaults: - tuned.utils.global_config.GlobalConfig.__init__.__defaults__ = ( - tuned.consts.GLOBAL_CONFIG_FILE, - ) - if orig_gc_load_config_defaults: - tuned.utils.global_config.GlobalConfig.load_config.__defaults__ = ( - tuned.consts.GLOBAL_CONFIG_FILE, - ) - # this call fails on ubuntu and containers, so mock it for testing - import pyudev.monitor - - orig_set_receive_buffer_size = pyudev.monitor.Monitor.set_receive_buffer_size - pyudev.monitor.Monitor.set_receive_buffer_size = lambda self, size: None - - def test_cleanup(): - if "TEST_ROOT_DIR" not in os.environ: - shutil.rmtree(test_root_dir) - for cnst, val in orig_consts.items(): - tuned.consts.__dict__[cnst] = val - for idx, dname in enumerate(orig_load_dirs): - tuned.consts.LOAD_DIRECTORIES[idx] = dname - for idx, dname in enumerate(orig_rec_dirs): - tuned.consts.RECOMMEND_DIRECTORIES[idx] = dname - if has_func: - tuned.utils.global_config.GlobalConfig.__init__.__func__.__defaults__ = ( - orig_gc_init_defaults - ) - tuned.utils.global_config.GlobalConfig.load_config.__func__.__defaults__ = ( - orig_gc_load_config_defaults - ) - else: - tuned.utils.global_config.GlobalConfig.__init__.__defaults__ = ( - orig_gc_init_defaults - ) - tuned.utils.global_config.GlobalConfig.load_config.__defaults__ = ( - orig_gc_load_config_defaults - ) - pyudev.monitor.Monitor.set_receive_buffer_size = orig_set_receive_buffer_size - - if "TEST_ROOT_DIR" not in os.environ: - atexit.register(test_cleanup) - return test_cleanup - - -def get_tuned_config(): - """get the tuned config and set our parameters in it""" - tuned_config = tuned.utils.global_config.GlobalConfig() - tuned_config.set("daemon", 0) - tuned_config.set("reapply_sysctl", 0) - tuned_config.set("dynamic_tuning", 0) - return tuned_config - - -def load_current_profile(tuned_config, tuned_profile, logger): - """load the current profile""" - tuned_app = None - errmsg = "Error loading tuned profile [{0}]".format(TUNED_PROFILE) - try: - tuned_app = tuned.daemon.Application(tuned_profile, tuned_config) - except TunedException as tex: - logger.debug("caught TunedException [{0}]".format(tex)) - errmsg = errmsg + ": {0}".format(tex) - tuned_app = None - except IOError as ioe: - # for testing this case, need to create a profile with a bad permissions e.g. - # mkdir ioerror_profile; touch ioerror_profile/tuned.conf; chmod 0000 !$ - logger.debug("caught IOError [{0}]".format(ioe)) - errmsg = errmsg + ": {0}".format(ioe) - tuned_app = None - if ( - tuned_app is None - or tuned_app.daemon is None - or tuned_app.daemon.profile is None - or tuned_app.daemon.profile.units is None - or tuned_app.daemon.profile.options is None - or "summary" not in tuned_app.daemon.profile.options - ): - tuned_app = None - if no_such_profile(): - errmsg = errmsg + ": Profile does not exist" - return tuned_app, errmsg - - -def validate_and_digest_item(sectionname, item, listallowed=True, allowempty=False): - """Validate an item - must contain only name, value, and state""" - tmpitem = item.copy() - name = tmpitem.pop("name", None) - value = tmpitem.pop("value", None) - state = tmpitem.pop("state", None) - errlist = [] - if name is None: - errlist.append(ERR_SECTION_MISSING_NAME.format(sectionname, item)) - elif not isinstance(name, ansible_six.string_types): - errlist.append(ERR_NAME_NOT_VALID.format(sectionname, name)) - elif (value is None and not allowempty) and state is None: - errlist.append(ERR_NO_VALUE_OR_STATE.format(sectionname, name)) - elif value is not None and state is not None: - errlist.append(ERR_BOTH_VALUE_AND_STATE.format(sectionname, name)) - elif state is not None and state != "absent": - errlist.append(ERR_STATE_ABSENT.format(sectionname, name, state)) - elif tmpitem: - errlist.append(ERR_UNEXPECTED_VALUES.format(sectionname, name, tmpitem)) - elif isinstance(value, list): - if not listallowed: - errlist.append(ERR_LIST_NOT_ALLOWED.format(sectionname, name, value)) - else: - for valitem in value: - if not isinstance(valitem, dict): - errlist.append( - ERR_VALUE_ITEM_NOT_DICT.format(sectionname, name, valitem) - ) - elif "previous" in valitem: - if valitem["previous"] != "replaced": - errlist.append( - ERR_VALUE_ITEM_PREVIOUS.format( - sectionname, name, valitem["previous"] - ) - ) - else: - item[SECTION_TO_REPLACE] = True - else: - tmperrlist = validate_and_digest_item( - sectionname, valitem, False, True - ) - errlist.extend(tmperrlist) - elif sectionname == "bootloader" and name == "cmdline": - errlist.append(ERR_BLCMD_MUST_BE_LIST.format(sectionname, name, value)) - elif isinstance(value, bool): - errlist.append(ERR_VALUE_CANNOT_BE_BOOLEAN.format(sectionname, name, value)) - return errlist - - -def validate_and_digest(params): - """Validate that params is in the correct format, since we - are using type `raw`, we have to perform the validation here. - Also do some pre-processing to make it easier to apply - the params to profile""" - errlist = [] - replaces = {} - for sectionname, items in params.items(): - if isinstance(items, dict): - if not items == REMOVE_SECTION_VALUE: - errlist.append( - ERR_REMOVE_SECTION_VALUE.format(sectionname, REMOVE_SECTION_VALUE) - ) - elif isinstance(items, list): - for item in items: - if not isinstance(item, dict): - errlist.append(ERR_ITEM_NOT_DICT.format(sectionname, item)) - elif not item: - continue # ignore empty items - elif "previous" in item: - if item["previous"] != "replaced": - errlist.append( - ERR_ITEM_PREVIOUS.format(sectionname, item["previous"]) - ) - else: - replaces[sectionname] = True - else: - itemerrlist = validate_and_digest_item(sectionname, item) - errlist.extend(itemerrlist) - else: - errlist.append(ERR_ITEM_DICT_OR_LIST.format(sectionname)) - if replaces: - params[SECTION_TO_REPLACE] = replaces - - return errlist - - -def remove_if_empty(params): - """recursively remove empty items from params - return true if params results in being empty, - false otherwise""" - if isinstance(params, list): - removed = 0 - for idx in range(0, len(params)): - realidx = idx - removed - if remove_if_empty(params[realidx]): - del params[realidx] - removed = removed + 1 - elif isinstance(params, dict): - for key, val in list(params.items()): - if remove_if_empty(val): - del params[key] - return params == [] or params == {} or params == "" or params is None - - -def run_module(): - """The entry point of the module.""" - - module_args = dict( - purge=dict(type="bool", required=False, default=False), - ) - tuned_plugin_names = get_supported_tuned_plugin_names() - for plugin_name in tuned_plugin_names: - # use raw here - type can be dict or list - perform validation - # below - module_args[plugin_name] = dict(type="raw", required=False) - module_args["ansible_managed_new"] = dict( - type="str", - required=True, - ) - module_args["ansible_managed_current"] = dict( - type="str", - required=True, - ) - - result = dict(changed=False, message="") - - module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) - - if not module.check_mode: - if os.environ.get("TESTING", "false") == "true": - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - _ = setup_for_testing() - - params = module.params - # remove any non-tuned fields from params and save them locally - # state = params.pop("state") - purge = params.pop("purge", False) - ansible_managed_new = params.pop("ansible_managed_new") - ansible_managed_current = params.pop("ansible_managed_current") - # also remove any empty or None - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - _ = remove_if_empty(params) - errlist = validate_and_digest(params) - if errlist: - errmsg = "Invalid format for input parameters" - module.fail_json(msg=errmsg, warnings=errlist, **result) - - # In check_mode, just perform input validation (above), because - # tuned will not be installed on the remote system - if module.check_mode: - module.exit_json(**result) - elif caught_import_error is not None: - raise caught_import_error # pylint: disable-msg=E0702 - elif caught_name_error is not None: - # name error is usually because tuned module was not imported - # but just in case, report it here - raise caught_name_error # pylint: disable-msg=E0702 - - tuned_config = get_tuned_config() - current_profile = None - tuned_app, errmsg = load_current_profile(tuned_config, TUNED_PROFILE, module) - - if tuned_app is None: - module.fail_json(msg=errmsg, **result) - else: - current_profile = tuned_app.daemon.profile - debug_print_profile(current_profile, module) - errmsg = "" - - result["msg"] = "Kernel settings were updated." - - # apply the given params to the profile - if there are any new items - # the function will return True we set changed = True - changestatus, reboot_required = apply_params_to_profile( - params, current_profile, purge - ) - profile_list = [] - if update_current_profile_and_mode(tuned_app.daemon, profile_list): - # profile or mode changed - if changestatus == NOCHANGES: - changestatus = CHANGES - result["msg"] = "Updated active profile and/or mode." - if changestatus == NOCHANGES and ansible_managed_new != ansible_managed_current: - changestatus = CHANGES - result["msg"] = "Updated ansible_managed." - if changestatus > NOCHANGES: - try: - write_profile(current_profile, ansible_managed_new) - # notify tuned to reload/reapply profile - except TunedException as tex: - module.debug("caught TunedException [{0}]".format(tex)) - errmsg = "Unable to apply tuned settings: {0}".format(tex) - module.fail_json(msg=errmsg, **result) - except IOError as ioe: - module.debug("caught IOError [{0}]".format(ioe)) - errmsg = "Unable to apply tuned settings: {0}".format(ioe) - module.fail_json(msg=errmsg, **result) - result["changed"] = True - else: - result["msg"] = "Kernel settings are up to date." - debug_print_profile(current_profile, module) - result["new_profile"] = profile_to_dict(current_profile) - result["active_profile"] = " ".join(profile_list) - result["reboot_required"] = reboot_required - if reboot_required: - result["msg"] = ( - result["msg"] + " A system reboot is needed to apply the changes." - ) - module.exit_json(**result) - - -def main(): - """The main function!""" - run_module() - - -if __name__ == "__main__": - main() diff --git a/library/kernel_settings_get_config.py b/library/kernel_settings_get_config.py new file mode 100644 index 0000000..8f693c3 --- /dev/null +++ b/library/kernel_settings_get_config.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Rich Megginson +# SPDX-License-Identifier: GPL-2.0-or-later +# +""" Parse ini or properties file """ + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: kernel_settings_get_config + +short_description: Parse given ini or properties file and return the data + +version_added: "2.13.0" + +description: + - "WARNING: Do not use this module directly! It is only for role internal use." + - Parse the given ini or properties file and return a dict + - The dict keys are the section names or DEFAULT + - The dict values are a list of dicts - each item has a name and a value + +options: + path: + description: Path to parse + required: true + type: str + +author: + - Rich Megginson (@richm) +""" + +EXAMPLES = """ +- name: Read ini file + kernel_settings_get_config: + path: /etc/tuned/kernel_settings/profile.ini + register: __kernel_settings_result +""" + +RETURN = """ +data: + description: dict of data - empty if file not found + returned: always + type: dict +""" + +try: + import configobj + + HAS_CONFIGOBJ = True +except ImportError: + HAS_CONFIGOBJ = False + +from ansible.module_utils.basic import AnsibleModule + + +def run_module(): + """The entry point of the module.""" + + module_args = dict( + path=dict(type="str", required=True), + ) + + result = dict(changed=False) + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) + + try: + cobj = configobj.ConfigObj(module.params["path"]) + data = cobj.dict() + result["data"] = data + except IOError: + result["data"] = {} + module.exit_json(**result) + + +def main(): + """The main function!""" + run_module() + + +if __name__ == "__main__": + main() diff --git a/pytest_extra_requirements.txt b/pytest_extra_requirements.txt index 8e11eaa..d1e7463 100644 --- a/pytest_extra_requirements.txt +++ b/pytest_extra_requirements.txt @@ -2,6 +2,6 @@ # Write extra requirements for running pytest here: # If you need ansible then uncomment the following line: --ransible_pytest_extra_requirements.txt +#-ransible_pytest_extra_requirements.txt # If you need mock then uncomment the following line: -mock ; python_version < "3.0" +#mock ; python_version < "3.0" diff --git a/tasks/main.yml b/tasks/main.yml index 624e4ed..fcd7576 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,4 +1,14 @@ --- +- name: Check sysctl settings for boolean values + fail: + msg: Boolean values are not allowed for sysctl settings + when: + - kernel_settings_sysctl != __kernel_settings_state_empty + - (kernel_settings_sysctl | selectattr("value", "defined") | + selectattr("value", "sameas", true) | list | length > 0) or + (kernel_settings_sysctl | selectattr("value", "defined") | + selectattr("value", "sameas", false) | list | length > 0) + - name: Set version specific variables include_tasks: set_vars.yml @@ -33,6 +43,32 @@ when: - kernel_settings_transactional_update_reboot_ok is none +- name: Get package_facts for tuned info + package_facts: + +- name: Set tuned version + set_fact: + __kernel_settings_tuned_version: "{{ + ansible_facts.packages['tuned'][0]['version'] }}" + +- name: Read tuned main config + kernel_settings_get_config: + path: "{{ __kernel_settings_tuned_main_conf_file }}" + register: __kernel_settings_register_tuned_main + +# this is the parent directory for the profile sub-directories +- name: Set tuned profile parent dir + set_fact: + __kernel_settings_profile_parent: "{{ __prof_from_conf + if __prof_from_conf | length > 0 else + __kernel_settings_tuned_dir ~ __prof_subdir }}" + vars: + __data: "{{ __kernel_settings_register_tuned_main.data }}" + __prof_from_conf: "{{ __data.get('profile_dirs', '').split(',')[-1] }}" + __prof_subdir: "{{ '/profiles' + if __kernel_settings_tuned_version is version('2.23.0', '>=') + else '' }}" + - name: Ensure required services are enabled and started service: name: "{{ item }}" @@ -46,96 +82,139 @@ state: directory mode: "0755" -- name: Generate a configuration for kernel settings - template: - src: "{{ __kernel_settings_profile_src }}.j2" - dest: "{{ __kernel_settings_profile_filename }}" - force: false - mode: "0644" +- name: Get active_profile + slurp: + path: "{{ __kernel_settings_tuned_active_profile }}" + register: __kernel_settings_tuned_current_profile + +- name: Set active_profile + set_fact: + __kernel_settings_active_profile: "{{ __active_profile }}" + vars: + __cur_profile: "{{ __kernel_settings_tuned_current_profile.content | + b64decode | trim }}" + __active_profile: "{{ __cur_profile + if __kernel_settings_tuned_profile in __cur_profile + else __cur_profile ~ ' ' ~ __kernel_settings_tuned_profile }}" + +- name: Ensure kernel_settings is in active_profile + copy: + content: > + {{ __kernel_settings_active_profile }} + dest: "{{ __kernel_settings_tuned_active_profile }}" + mode: preserve + register: __kernel_settings_register_profile + +- name: Set profile_mode to manual + copy: + content: > + manual + dest: "{{ __kernel_settings_tuned_profile_mode }}" + mode: preserve + register: __kernel_settings_register_mode - name: Get current config - slurp: - src: "{{ __kernel_settings_profile_filename }}" - register: __kernel_settings_profile_contents_b64 - changed_when: false + kernel_settings_get_config: + path: "{{ __kernel_settings_profile_filename }}" + register: __kernel_settings_profile_contents - name: Apply kernel settings - kernel_settings: - sysctl: "{{ kernel_settings_sysctl if kernel_settings_sysctl else omit }}" - sysfs: "{{ kernel_settings_sysfs if kernel_settings_sysfs else omit }}" - systemd: - - name: "{{ 'cpu_affinity' if kernel_settings_systemd_cpu_affinity - else none }}" - value: "{{ kernel_settings_systemd_cpu_affinity - if kernel_settings_systemd_cpu_affinity != - __kernel_settings_state_absent - else none }}" - state: "{{ 'absent' if kernel_settings_systemd_cpu_affinity == - __kernel_settings_state_absent else none }}" - vm: - - name: "{{ 'transparent_hugepages' - if kernel_settings_transparent_hugepages - else none }}" - value: "{{ kernel_settings_transparent_hugepages - if kernel_settings_transparent_hugepages != - __kernel_settings_state_absent else none }}" - state: "{{ 'absent' if kernel_settings_transparent_hugepages == - __kernel_settings_state_absent else none }}" - - name: "{{ 'transparent_hugepage.defrag' - if kernel_settings_transparent_hugepages_defrag - else none }}" - value: "{{ kernel_settings_transparent_hugepages_defrag - if kernel_settings_transparent_hugepages_defrag != - __kernel_settings_state_absent else none }}" - state: "{{ 'absent' if kernel_settings_transparent_hugepages_defrag == - __kernel_settings_state_absent else none }}" - bootloader: - - name: "{{ 'cmdline' - if kernel_settings_bootloader_cmdline | d({}) - else none }}" - value: "{{ kernel_settings_bootloader_cmdline | - d([]) }}" - purge: "{{ kernel_settings_purge }}" - ansible_managed_new: "{{ __kernel_settings_new_header }}" - ansible_managed_current: "{{ __kernel_settings_cur_header }}" - notify: __kernel_settings_handler_modified - register: __kernel_settings_register_module - when: - - kernel_settings_sysctl or kernel_settings_sysfs - or kernel_settings_systemd_cpu_affinity - or kernel_settings_transparent_hugepages - or kernel_settings_transparent_hugepages_defrag - or kernel_settings_bootloader_cmdline | d({}) - or kernel_settings_purge - or __kernel_settings_cur_header != __kernel_settings_new_header + template: + src: "{{ __kernel_settings_profile_src }}.j2" + dest: "{{ __kernel_settings_profile_filename }}" + mode: "0644" vars: - __kernel_settings_cur_header: "{{ - __kernel_settings_profile_contents_b64.content | b64decode | - regex_replace('(?sm)^\\[.*$', '') }}" - __kernel_settings_new_header: "{{ - lookup('template', 'get_ansible_managed.j2') }}" + __sysctl_old: "{{ __kernel_settings_profile_contents.data.get('sysctl', {}) + if not kernel_settings_purge + and kernel_settings_sysctl != __kernel_settings_state_empty + and not __kernel_settings_previous_replaced in kernel_settings_sysctl + else {} }}" + __sysfs_old: "{{ __kernel_settings_profile_contents.data.get('sysfs', {}) + if not kernel_settings_purge + and kernel_settings_sysfs != __kernel_settings_state_empty + and not __kernel_settings_previous_replaced in kernel_settings_sysfs + else {} }}" + __systemd_old: "{{ + __kernel_settings_profile_contents.data.get('systemd', {}).get('cpu_affinity') + if not kernel_settings_purge + and kernel_settings_systemd_cpu_affinity != __kernel_settings_state_absent + else none }}" + __trans_huge_old: "{{ + __kernel_settings_profile_contents.data.get('vm', {}).get('transparent_hugepages') + if not kernel_settings_purge + and kernel_settings_transparent_hugepages != __kernel_settings_state_absent + else none }}" + __trans_defrag_old: "{{ + __kernel_settings_profile_contents.data.get('vm', {}).get('transparent_hugepage.defrag') + if not kernel_settings_purge + and kernel_settings_transparent_hugepages_defrag != __kernel_settings_state_absent + else none }}" + __sections: + - name: sysctl + new: "{{ kernel_settings_sysctl | difference([__kernel_settings_previous_replaced]) | list + if kernel_settings_sysctl != __kernel_settings_state_empty + else [] }}" + old: "{{ __sysctl_old }}" + - name: sysfs + new: "{{ kernel_settings_sysfs | difference([__kernel_settings_previous_replaced]) | list + if kernel_settings_sysfs != __kernel_settings_state_empty + else [] }}" + old: "{{ __sysfs_old }}" + - name: systemd + new: + - name: cpu_affinity + value: "{{ kernel_settings_systemd_cpu_affinity }}" + old: + cpu_affinity: "{{ __systemd_old }}" + - name: vm + new: + - name: transparent_hugepages + value: "{{ kernel_settings_transparent_hugepages }}" + - name: transparent_hugepage.defrag + value: "{{ kernel_settings_transparent_hugepages_defrag }}" + old: + transparent_hugepages: "{{ __trans_huge_old }}" + transparent_hugepage.defrag: "{{ __trans_defrag_old }}" + register: __kernel_settings_register_apply + +# this will also apply the kernel_settings profile, so we +# can skip the apply profile step in this case +- name: Restart tuned to apply active profile, mode changes + service: + name: "{{ item }}" + state: restarted + enabled: true + loop: "{{ __kernel_settings_services }}" + when: __kernel_settings_register_profile is changed or + __kernel_settings_register_mode is changed - name: Tuned apply settings - command: > - tuned-adm profile '{{ __kernel_settings_register_module.active_profile }}' - when: __kernel_settings_register_module is changed # noqa no-handler + command: >- + tuned-adm profile {{ __kernel_settings_active_profile | quote }} + when: + - not __kernel_settings_register_profile is changed + - not __kernel_settings_register_mode is changed + - __kernel_settings_register_apply is changed # noqa no-handler changed_when: true - name: Verify settings include_tasks: verify_settings.yml - when: __kernel_settings_register_module is changed # noqa no-handler - -- name: Notify user that reboot is needed to apply changes - debug: - msg: > - kernel_settings have been modified. - A reboot is required in order to apply the changes. - when: - - __kernel_settings_register_module.reboot_required | d(false) - - not kernel_settings_reboot_ok | d(false) + when: __kernel_settings_register_apply is changed # noqa no-handler +# reboot not currently used - was used when the role could set +# some bootloader settings, but that was never supported, and +# we now have a dedicated bootloader role which is much better. +# The sysctl, sysfs, etc. settings are applied immediately, +# there is no need to reboot to apply those changes. +# so, keep this here since it is part of the public API, and +# in case we need it in the future - name: Set the flag that reboot is needed to apply changes set_fact: - kernel_settings_reboot_required: true - when: - - __kernel_settings_register_module.reboot_required | d(false) + kernel_settings_reboot_required: false + +- name: Set flag to indicate changed for testing + set_fact: + __kernel_settings_changed: "{{ + __kernel_settings_register_profile is changed + or __kernel_settings_register_mode is changed + or __kernel_settings_register_apply is changed }}" diff --git a/templates/kernel_settings.j2 b/templates/kernel_settings.j2 index e80e380..9db8307 100644 --- a/templates/kernel_settings.j2 +++ b/templates/kernel_settings.j2 @@ -1,4 +1,33 @@ {{ ansible_managed | comment }} {{ "system_role:kernel_settings" | comment(prefix="", postfix="") }} [main] -summary=kernel settings +summary = kernel settings +{% set __settings = {} %} +{% for section in __sections %} +{% set section_name = section["name"] %} +{% for item in section["new"] %} +{% if item.state | d() == "absent" %} +{% set _ = __settings.setdefault(section_name, {}).__setitem__(item.name, __kernel_settings_state_absent) %} +{% elif item.value != none and item.value != "" %} +{% set _ = __settings.setdefault(section_name, {}).__setitem__(item.name, item.value) %} +{% endif %} +{% endfor %} +{% for key, value in section["old"].items() %} +{% if not __settings.get(section_name, {}).__contains__(key) and value != none and value != "" %} +{% set _ = __settings.setdefault(section_name, {}).__setitem__(key, value) %} +{% endif %} +{% endfor %} +{% endfor %} +{% set seen_sections = {} %} +{% for section_name in __settings.keys() | sort %} +{% set section = __settings[section_name] %} +{% for key in section.keys() | sort %} +{% if section[key] != __kernel_settings_state_absent %} +{% if not seen_sections.__contains__(section_name) %} +{% set _ = seen_sections.__setitem__(section_name, true) %} +[{{ section_name }}] +{% endif %} +{{ key }} = {{ section[key] }} +{% endif %} +{% endfor %} +{% endfor %} diff --git a/tests/install_ansible_for_testing.sh b/tests/install_ansible_for_testing.sh deleted file mode 100755 index b407e86..0000000 --- a/tests/install_ansible_for_testing.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# this is used to install ansible into a venv or tox env -# for testing purposes - -set -euo pipefail - -if [ -n "$1" ] ; then - INSTALL_DIR="$1" -fi -if [ -n "${2:-}" ] ; then - ANSIBLE_VERSION="$2" -else - ANSIBLE_VERSION="${ANSIBLE_VERSION:-devel}" -fi - -ANSIBLE_FULL_URL="${ANSIBLE_FULL_URL:-https://github.com/ansible-collection-migration/ansible-base.git}" - -if [ "${REINSTALL:-false}" = true ] ; then - rm -rf "$INSTALL_DIR" -fi -if [ ! -d "$INSTALL_DIR" ] ; then - git clone "$ANSIBLE_FULL_URL" -b "$ANSIBLE_VERSION" "$INSTALL_DIR" - # install dependencies required for ansible - pip install -r"$INSTALL_DIR/requirements.txt" -else - echo Using already installed "ansible-$ANSIBLE_VERSION" in "$INSTALL_DIR" -fi diff --git a/tests/install_tuned_for_testing.sh b/tests/install_tuned_for_testing.sh deleted file mode 100755 index 4e16bda..0000000 --- a/tests/install_tuned_for_testing.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# this is used to install tuned into a venv or tox env -# for testing purposes - -set -euo pipefail - -# import from utils.sh -. "${LSR_SCRIPTDIR}/utils.sh" - -function ks_lsr_get_site_packages_dir() { - python -c 'import sys; print([xx for xx in sys.path if xx.endswith("site-packages")][0])' -} - -function ks_lsr_install_tuned() { - local pyver="$1" - local site_pkg_dir="$2" - local tunedver - case $pyver in - 2.*) tunedver=2.11.0;; - 3.*) tunedver=2.15.0;; - *) echo unknown python version "$pyver"; exit 1;; - esac - # these are required in order to install pip packages from source - pip install --upgrade pip setuptools wheel - # extra requirements for tuned - pip install -r "$TOPDIR/tuned_requirements.txt" - if [ "${REINSTALL:-false}" = true ] ; then - rm -rf "$site_pkg_dir/tuned" - fi - if [ ! -d "$site_pkg_dir/tuned" ] ; then - curl -s -L -o "$site_pkg_dir/tuned.tgz" "${TUNED_FULL_URL:-${TUNED_BASE_URL}/v${tunedver}.tar.gz}" - # extract only the tuned python code - tar -C "$site_pkg_dir" -x -z -f "$site_pkg_dir/tuned.tgz" --strip-components=1 "tuned-${tunedver}/tuned" - rm -rf "$site_pkg_dir/tuned.tgz" - echo Installed "tuned-$tunedver" in "$site_pkg_dir/tuned" - else - echo Using already installed "tuned-$tunedver" in "$site_pkg_dir/tuned" - fi -} - -TUNED_BASE_URL="${TUNED_BASE_URL:-https://github.com/redhat-performance/tuned/archive}" -# install tuned only where needed -KS_LSR_PYVER=$(lsr_get_python_version "$(type -p python)") -if lsr_compare_versions "$KS_LSR_PYVER" -lt 2.7 ; then - # tuned is not supported on python 2.6 when installing the dependencies - # from pypi - for some of the dependencies, such as PyGObject, there are - # no versions which are available on pypi that work on python 2.6 - lsr_info kernel_settings tests not supported on python "$KS_LSR_PYVER" - skipping - exit 0 -fi -KS_LSR_NEED_TUNED=0 -case "${LSR_TOX_ENV_NAME:-}" in - pylint) KS_LSR_NEED_TUNED=1 ;; - py*) KS_LSR_NEED_TUNED=1 ;; - coveralls) KS_LSR_NEED_TUNED=1 ;; - flake8) KS_LSR_NEED_TUNED=1 ;; -esac -if [ "$KS_LSR_NEED_TUNED" = 1 ] ; then - ks_lsr_install_tuned "$KS_LSR_PYVER" "$(ks_lsr_get_site_packages_dir)" -fi diff --git a/tests/roles/caller/tasks/main.yml b/tests/roles/caller/tasks/main.yml index bf7f471..1bc527e 100644 --- a/tests/roles/caller/tasks/main.yml +++ b/tests/roles/caller/tasks/main.yml @@ -4,6 +4,7 @@ - name: Include test role include_role: name: "{{ roletoinclude }}" + public: true - name: Assert variable not overridden assert: diff --git a/tests/run_ansible_for_testing.sh b/tests/run_ansible_for_testing.sh deleted file mode 100755 index ea2f3e3..0000000 --- a/tests/run_ansible_for_testing.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -ANSIBLE_DIR="$1" ; shift -source "$ANSIBLE_DIR/hacking/env-setup" -"$@" diff --git a/tests/tasks/cleanup.yml b/tests/tasks/cleanup.yml new file mode 100644 index 0000000..b89e3a6 --- /dev/null +++ b/tests/tasks/cleanup.yml @@ -0,0 +1,72 @@ +--- +- name: Show current tuned profile settings + command: cat {{ __kernel_settings_profile_filename }} + changed_when: false + ignore_errors: true # noqa ignore-errors + +- name: Use role purge to remove settings + block: + - name: Run role with purge to remove everything + include_role: + name: linux-system-roles.kernel_settings + vars: + kernel_settings_purge: true + kernel_settings_sysctl: [] + kernel_settings_sysfs: [] + kernel_settings_systemd_cpu_affinity: null + kernel_settings_transparent_hugepages: null + kernel_settings_transparent_hugepages_defrag: null + + - name: Verify no settings + shell: | + set -euxo pipefail + exec 1>&2 + rc=0 + conf={{ __kernel_settings_profile_filename }} + for section in sysctl sysfs systemd vm; do + if grep ^\\["$section"\\] "$conf"; then + echo ERROR: "$section" settings present + rc=1 + fi + done + exit "$rc" + changed_when: false + + always: + - name: Remove kernel_settings tuned profile + file: + path: "{{ __kernel_settings_profile_dir }}" + state: absent + + - name: Get active_profile + slurp: + path: "{{ __kernel_settings_tuned_active_profile }}" + register: __kernel_settings_tuned_current_profile + + - name: Ensure kernel_settings is not in active_profile + copy: + content: > + {{ __active_profile }} + dest: "{{ __kernel_settings_tuned_active_profile }}" + mode: preserve + vars: + __cur_profile: "{{ __kernel_settings_tuned_current_profile.content | + b64decode | trim }}" + # noqa jinja[spacing] + __active_profile: "{{ + __cur_profile.replace(' kernel_settings ', ' '). + replace('kernel_settings ', '').replace(' kernel_settings', '') }}" + + - name: Set profile_mode to auto + copy: + content: > + auto + dest: "{{ __kernel_settings_tuned_profile_mode }}" + mode: preserve + + - name: Restart tuned + service: + name: "{{ item }}" + state: started + enabled: true + loop: "{{ __kernel_settings_services }}" diff --git a/tests/tests_bool_not_allowed.yml b/tests/tests_bool_not_allowed.yml index 91d065d..d287170 100644 --- a/tests/tests_bool_not_allowed.yml +++ b/tests/tests_bool_not_allowed.yml @@ -2,34 +2,20 @@ - name: Test boolean values not allowed hosts: all tasks: - - name: Validate no boolean values for bootloader cmdline - block: - - name: Try to pass a boolean value for bootloader cmdline - include_role: - name: linux-system-roles.kernel_settings - vars: - kernel_settings_bootloader_cmdline: - - name: somevalue - value: yes # yamllint disable-line rule:truthy - - - name: Unreachable task - fail: - msg: UNREACH - - rescue: - - name: Check for bootloader cmdline bool value error - assert: - that: ansible_failed_result.msg != 'UNREACH' - - name: Validate no boolean values for sysctl values block: - name: Try to pass a boolean value for sysctl value include_role: name: linux-system-roles.kernel_settings + public: true vars: kernel_settings_sysctl: + - name: valid_value + value: "true" - name: somevalue value: yes # yamllint disable-line rule:truthy + - name: anothervalue + value: no # yamllint disable-line rule:truthy - name: Unreachable task fail: @@ -39,3 +25,9 @@ - name: Check for sysctl bool value error assert: that: ansible_failed_result.msg != 'UNREACH' + + always: + - name: Cleanup + tags: + - tests::cleanup + include_tasks: tasks/cleanup.yml diff --git a/tests/tests_change_settings.yml b/tests/tests_change_settings.yml index 97f445a..b57ce6a 100644 --- a/tests/tests_change_settings.yml +++ b/tests/tests_change_settings.yml @@ -4,182 +4,216 @@ tags: - tests::reboot tasks: - - name: Determine if system is ostree and set flag - when: not __kernel_settings_is_ostree is defined + - name: Run test block: - - name: Check if system is ostree + - name: Determine if system is ostree and set flag + when: not __kernel_settings_is_ostree is defined + block: + - name: Check if system is ostree + stat: + path: /run/ostree-booted + register: __ostree_booted_stat + + - name: Set flag to indicate system is ostree + set_fact: + __kernel_settings_is_ostree: "{{ + __ostree_booted_stat.stat.exists }}" + + # procps-ng for sysctl cmd + - name: Ensure required packages are installed + package: + name: [tuned, procps-ng] + state: present + use: "{{ (__kernel_settings_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" + + - name: See if tuned has a profile subdir stat: - path: /run/ostree-booted - register: __ostree_booted_stat + path: /etc/tuned/profiles + register: __tuned_profiles - - name: Set flag to indicate system is ostree + - name: Set profile dir set_fact: - __kernel_settings_is_ostree: "{{ - __ostree_booted_stat.stat.exists }}" - - # procps-ng for sysctl cmd - - name: Ensure required packages are installed - package: - name: [tuned, procps-ng] - state: present - use: "{{ (__kernel_settings_is_ostree | d(false)) | - ternary('ansible.posix.rhel_rpm_ostree', omit) }}" - - - name: Ensure kernel settings profile directory exists - file: - path: /etc/tuned/kernel_settings - state: directory - mode: "0755" - - - name: Generate a configuration for kernel settings - copy: - src: tuned/etc/tuned/change_settings/tuned.conf - dest: /etc/tuned/kernel_settings/tuned.conf - mode: "0644" - - - name: Ensure required services are enabled and started - service: - name: tuned - state: restarted - enabled: true - - - name: Apply kernel_settings - include_role: - name: linux-system-roles.kernel_settings - vars: - kernel_settings_sysctl: - - name: fs.file-max - value: 400000 - - name: kernel.threads-max - state: absent - kernel_settings_sysfs: - - name: /sys/class/net/lo/mtu - value: 65000 - - - name: Ensure kernel_settings_reboot_required is unset or undefined - assert: - that: not kernel_settings_reboot_required | d(false) - - - name: Check sysfs after role runs - command: grep -x 65000 /sys/class/net/lo/mtu - changed_when: false - - - name: Check sysctl after role runs - shell: |- - set -euo pipefail - sysctl -n fs.file-max | grep -x 400000 - changed_when: false - - - name: Check sysctl after role runs - shell: |- - set -euo pipefail - sysctl -n kernel.threads-max | grep -Lxvq 29968 - changed_when: false - - - name: Reboot the machine - see if settings persist after reboot - reboot: - test_command: tuned-adm active - - - name: Check sysctl after reboot - shell: |- - set -euo pipefail - sysctl -n fs.file-max | grep -x 400000 - changed_when: false - - - name: Check sysfs after reboot - command: grep -x 65000 /sys/class/net/lo/mtu - changed_when: false - - - name: Show cmdline - command: cat /proc/cmdline - changed_when: false - - - name: Check sysctl after reboot - shell: |- - set -euo pipefail - sysctl -n kernel.threads-max | grep -Lxvq 29968 - changed_when: false - - - name: Check with tuned verify - command: tuned-adm verify -i - changed_when: false - - - name: Apply role again and remove settings - include_role: - name: linux-system-roles.kernel_settings - vars: - kernel_settings_reboot_ok: true - kernel_settings_sysctl: - - name: fs.file-max - state: absent - - name: kernel.threads-max - state: absent - - - name: Force handlers - meta: flush_handlers - - - name: Ensure kernel_settings_reboot_required is not set or is false - assert: - that: not kernel_settings_reboot_required | d(false) - - - name: Check sysctl after reboot - shell: |- - set -euo pipefail - sysctl -n fs.file-max | grep -Lxvq 400000 - changed_when: false - - - name: Apply kernel_settings for removing - include_role: - name: linux-system-roles.kernel_settings - vars: - kernel_settings_reboot_ok: true - kernel_settings_sysctl: - - name: fs.file-max - value: 400001 - kernel_settings_sysfs: - - name: /sys/class/net/lo/mtu - value: 60666 - - - name: Force handlers - meta: flush_handlers - - - name: Ensure kernel_settings_reboot_required is not set or is false - assert: - that: not kernel_settings_reboot_required | d(false) - - - name: Check sysctl - shell: |- - set -euo pipefail - sysctl -n fs.file-max | grep -qx 400001 - changed_when: false - - - name: Check sysfs after role runs - command: grep -x 60666 /sys/class/net/lo/mtu - changed_when: false - - - name: Apply kernel_settings for removing section - include_role: - name: linux-system-roles.kernel_settings - vars: - kernel_settings_reboot_ok: true - kernel_settings_sysctl: - state: empty - kernel_settings_sysfs: - state: empty - - - name: Force handlers - meta: flush_handlers - - - name: Ensure kernel_settings_reboot_required is not set or is false - assert: - that: not kernel_settings_reboot_required | d(false) - - - name: Check sysctl - shell: |- - set -euo pipefail - sysctl -n fs.file-max | grep -Lvxq 400001 - changed_when: false - - - name: Check sysfs after role runs - command: grep -Lxqv 60666 /sys/class/net/lo/mtu - changed_when: false + __profile_dir: "{{ __dir ~ '/kernel_settings' }}" + vars: + __dir: "{{ '/etc/tuned/profiles' if __tuned_profiles.stat.exists + else '/etc/tuned' }}" + + - name: Ensure kernel settings profile directory exists + file: + path: "{{ __profile_dir }}" + state: directory + mode: "0755" + + - name: Generate a configuration for kernel settings + copy: + src: tuned/etc/tuned/change_settings/tuned.conf + dest: "{{ __profile_dir }}/tuned.conf" + mode: "0644" + + - name: Ensure required services are enabled and started + service: + name: tuned + state: restarted + enabled: true + + - name: Apply kernel_settings + include_role: + name: linux-system-roles.kernel_settings + public: true + vars: + kernel_settings_sysctl: + - name: fs.file-max + value: 400000 + - name: kernel.threads-max + state: absent + kernel_settings_sysfs: + - name: /sys/class/net/lo/mtu + value: 65000 + + - name: Ensure kernel_settings_reboot_required is unset or undefined + assert: + that: not kernel_settings_reboot_required | d(false) + + - name: Ensure role reported changed + assert: + that: __kernel_settings_changed | d(false) + + - name: Check sysfs after role runs + command: grep -x 65000 /sys/class/net/lo/mtu + changed_when: false + + - name: Check sysctl after role runs + shell: |- + set -euo pipefail + sysctl -n fs.file-max | grep -x 400000 + changed_when: false + + - name: Check sysctl after role runs + shell: |- + set -euo pipefail + sysctl -n kernel.threads-max | grep -Lxvq 29968 + changed_when: false + + - name: Reboot the machine - see if settings persist after reboot + reboot: + test_command: tuned-adm active + + - name: Check sysctl after reboot + shell: |- + set -euo pipefail + sysctl -n fs.file-max | grep -x 400000 + changed_when: false + + - name: Check sysfs after reboot + command: grep -x 65000 /sys/class/net/lo/mtu + changed_when: false + + - name: Check sysctl after reboot + shell: |- + set -euo pipefail + sysctl -n kernel.threads-max | grep -Lxvq 29968 + changed_when: false + + - name: Check with tuned verify + command: tuned-adm verify -i + changed_when: false + + - name: Apply role again and remove settings + include_role: + name: linux-system-roles.kernel_settings + vars: + kernel_settings_reboot_ok: true + kernel_settings_sysctl: + - name: fs.file-max + state: absent + - name: kernel.threads-max + state: absent + + - name: Force handlers + meta: flush_handlers + + - name: Ensure kernel_settings_reboot_required is not set or is false + assert: + that: not kernel_settings_reboot_required | d(false) + + - name: Ensure role reported changed + assert: + that: __kernel_settings_changed | d(false) + + - name: Check sysctl after reboot + shell: |- + set -euo pipefail + sysctl -n fs.file-max | grep -Lxvq 400000 + changed_when: false + + - name: Apply kernel_settings for removing + include_role: + name: linux-system-roles.kernel_settings + vars: + kernel_settings_reboot_ok: true + kernel_settings_sysctl: + - name: fs.file-max + value: 400001 + kernel_settings_sysfs: + - name: /sys/class/net/lo/mtu + value: 60666 + + - name: Force handlers + meta: flush_handlers + + - name: Ensure kernel_settings_reboot_required is not set or is false + assert: + that: not kernel_settings_reboot_required | d(false) + + - name: Ensure role reported changed + assert: + that: __kernel_settings_changed | d(false) + + - name: Check sysctl + shell: |- + set -euo pipefail + sysctl -n fs.file-max | grep -qx 400001 + changed_when: false + + - name: Check sysfs after role runs + command: grep -x 60666 /sys/class/net/lo/mtu + changed_when: false + + - name: Apply kernel_settings for removing section + include_role: + name: linux-system-roles.kernel_settings + vars: + kernel_settings_reboot_ok: true + kernel_settings_sysctl: + state: empty + kernel_settings_sysfs: + state: empty + kernel_settings_transparent_hugepages: never + + - name: Force handlers + meta: flush_handlers + + - name: Ensure kernel_settings_reboot_required is not set or is false + assert: + that: not kernel_settings_reboot_required | d(false) + + - name: Ensure role reported changed + assert: + that: __kernel_settings_changed | d(false) + + - name: Check sysctl + shell: |- + set -euo pipefail + sysctl -n fs.file-max | grep -Lvxq 400001 + changed_when: false + + - name: Check sysfs after role runs + command: grep -Lxqv 60666 /sys/class/net/lo/mtu + changed_when: false + + always: + - name: Cleanup + tags: + - tests::cleanup + include_tasks: tasks/cleanup.yml diff --git a/tests/tests_default.yml b/tests/tests_default.yml index a4d98f3..d47b33a 100644 --- a/tests/tests_default.yml +++ b/tests/tests_default.yml @@ -2,5 +2,16 @@ - name: Ensure that the role runs with default parameters hosts: all gather_facts: false - roles: - - linux-system-roles.kernel_settings + tasks: + - name: Run test + block: + - name: Run role with no settings + include_role: + name: linux-system-roles.kernel_settings + public: true + + always: + - name: Cleanup + tags: + - tests::cleanup + include_tasks: tasks/cleanup.yml diff --git a/tests/tests_include_vars_from_parent.yml b/tests/tests_include_vars_from_parent.yml index f012519..11bf78c 100644 --- a/tests/tests_include_vars_from_parent.yml +++ b/tests/tests_include_vars_from_parent.yml @@ -3,44 +3,63 @@ hosts: all gather_facts: true tasks: - - name: Create var file in caller that can override the one in called role - delegate_to: localhost - copy: - # usually the fake file will cause the called role to crash of - # overriding happens, but if not, set a variable that will - # allow to detect the bug - content: "__caller_override: true" - # XXX ugly, self-modifying code - changes the "caller" role on - # the controller - dest: "{{ playbook_dir }}/roles/caller/vars/{{ item }}.yml" - mode: preserve - loop: "{{ varfiles | unique }}" - # In case the playbook is executed against multiple hosts, use - # only the first one. Otherwise the hosts would stomp on each - # other since they are changing files on the controller. - when: inventory_hostname == ansible_play_hosts_all[0] - vars: - # change to hostvars['localhost']['ansible_facts'] to use the - # information for localhost - facts: "{{ ansible_facts }}" - versions: - - "{{ facts['distribution_version'] }}" - - "{{ facts['distribution_major_version'] }}" - separators: ["-", "_"] - # create all variants like CentOS, CentOS_8.1, CentOS-8.1, - # CentOS-8, CentOS-8.1 - # more formally: - # {{ ansible_distribution }}-{{ ansible_distribution_version }} - # {{ ansible_distribution }}-{{ ansible_distribution_major_version }} - # {{ ansible_distribution }} - # {{ ansible_os_family }} - # and the same for _ as separator. - varfiles: "{{ [facts['distribution']] | product(separators) | - map('join') | product(versions) | map('join') | list + - [facts['distribution'], facts['os_family']] }}" + - name: Run test + block: + - name: >- + Create var file in caller that can override the one in called role + delegate_to: localhost + copy: + # usually the fake file will cause the called role to crash of + # overriding happens, but if not, set a variable that will + # allow to detect the bug + content: "__caller_override: true" + # XXX ugly, self-modifying code - changes the "caller" role on + # the controller + dest: "{{ playbook_dir }}/roles/caller/vars/{{ item }}.yml" + mode: preserve + loop: "{{ varfiles | unique }}" + # In case the playbook is executed against multiple hosts, use + # only the first one. Otherwise the hosts would stomp on each + # other since they are changing files on the controller. + when: inventory_hostname == ansible_play_hosts_all[0] + vars: + # change to hostvars['localhost']['ansible_facts'] to use the + # information for localhost + facts: "{{ ansible_facts }}" + versions: + - "{{ facts['distribution_version'] }}" + - "{{ facts['distribution_major_version'] }}" + separators: ["-", "_"] + # create all variants like CentOS, CentOS_8.1, CentOS-8.1, + # CentOS-8, CentOS-8.1 + # more formally: + # {{ ansible_distribution }}-{{ ansible_distribution_version }} + # {{ ansible_distribution }}-{{ ansible_distribution_major_version }} + # {{ ansible_distribution }} + # {{ ansible_os_family }} + # and the same for _ as separator. + varfiles: "{{ [facts['distribution']] | product(separators) | + map('join') | product(versions) | map('join') | list + + [facts['distribution'], facts['os_family']] }}" + register: __varfiles_created - - name: Import caller role - import_role: - name: caller - vars: - roletoinclude: linux-system-roles.kernel_settings + - name: Import caller role + import_role: + name: caller + vars: + roletoinclude: linux-system-roles.kernel_settings + always: + - name: Cleanup role + tags: + - tests::cleanup + include_tasks: tasks/cleanup.yml + + - name: Cleanup test + tags: + - tests::cleanup + file: + path: "{{ item.dest }}" + state: absent + loop: "{{ __varfiles_created.results }}" + delegate_to: localhost + when: inventory_hostname == ansible_play_hosts_all[0] diff --git a/tests/tests_simple_settings.yml b/tests/tests_simple_settings.yml index 263f2f5..363e42a 100644 --- a/tests/tests_simple_settings.yml +++ b/tests/tests_simple_settings.yml @@ -5,55 +5,71 @@ tags: - tests::reboot tasks: - - name: Set platform independent vars used by this test - include_vars: - file: vars/vars_simple_settings.yml - - - name: Disable bootloader cmdline testing on Fedora - set_fact: - kernel_settings_bootloader_cmdline: null - __kernel_settings_blcmdline_value: null - __kernel_settings_check_reboot: false - when: ansible_distribution == "Fedora" - - # use public: true here so that the private role - # variables will be exported - we use - # __kernel_settings_profile_filename to verify - # that the settings were applied correctly - - name: Apply the settings - call the role - include_role: - name: linux-system-roles.kernel_settings - public: true - - - name: Verify that settings were applied correctly - include_tasks: tasks/assert_kernel_settings.yml - vars: - __kernel_settings_test_verify: false - - - name: Check ansible_managed, fingerprint in generated files - include_tasks: tasks/check_header.yml - vars: - __file: "{{ __kernel_settings_profile_filename }}" - __fingerprint: "system_role:kernel_settings" - - # reboot if a reboot is needed AND we did not tell the role - # to reboot the machine if needed - - name: Reboot the machine - see if settings persist after reboot - reboot: - test_command: tuned-adm active - when: - - kernel_settings_reboot_required | d(false) - - not kernel_settings_test_reboot_ok | d(false) - - - name: Verify that settings were applied correctly after reboot - include_tasks: tasks/assert_kernel_settings.yml - vars: - __kernel_settings_check_reboot: false - __kernel_settings_test_verify: true - when: - - kernel_settings_reboot_required | d(false) - - not kernel_settings_test_reboot_ok | d(false) - - - name: Show contents of tuned.conf - command: cat /etc/tuned/kernel_settings/tuned.conf - changed_when: false + - name: Run test + block: + - name: Set platform independent vars used by this test + include_vars: + file: vars/vars_simple_settings.yml + + - name: Disable bootloader cmdline testing on Fedora + set_fact: + kernel_settings_bootloader_cmdline: null + __kernel_settings_blcmdline_value: null + __kernel_settings_check_reboot: false + when: ansible_distribution == "Fedora" + + # use public: true here so that the private role + # variables will be exported - we use + # __kernel_settings_profile_filename to verify + # that the settings were applied correctly + - name: Apply the settings - call the role + include_role: + name: linux-system-roles.kernel_settings + public: true + + - name: Verify that settings were applied correctly + include_tasks: tasks/assert_kernel_settings.yml + vars: + __kernel_settings_test_verify: false + + - name: Check ansible_managed, fingerprint in generated files + include_tasks: tasks/check_header.yml + vars: + __file: "{{ __kernel_settings_profile_filename }}" + __fingerprint: "system_role:kernel_settings" + + - name: Ensure role reported changed + assert: + that: __kernel_settings_changed | d(false) + + # reboot if a reboot is needed AND we did not tell the role + # to reboot the machine if needed + - name: Reboot the machine - see if settings persist after reboot + reboot: + test_command: tuned-adm active + when: + - kernel_settings_reboot_required | d(false) + - not kernel_settings_test_reboot_ok | d(false) + + - name: Verify that settings were applied correctly after reboot + include_tasks: tasks/assert_kernel_settings.yml + vars: + __kernel_settings_check_reboot: false + __kernel_settings_test_verify: true + when: + - kernel_settings_reboot_required | d(false) + - not kernel_settings_test_reboot_ok | d(false) + + - name: Apply the settings again to check idempotency + include_role: + name: linux-system-roles.kernel_settings + + - name: Ensure role reported not changed + assert: + that: not __kernel_settings_changed | d(false) + + always: + - name: Cleanup + tags: + - tests::cleanup + include_tasks: tasks/cleanup.yml diff --git a/tests/tuned/etc/tuned/basic_settings/tuned.conf b/tests/tuned/etc/tuned/basic_settings/tuned.conf deleted file mode 100644 index 368e81c..0000000 --- a/tests/tuned/etc/tuned/basic_settings/tuned.conf +++ /dev/null @@ -1,24 +0,0 @@ -# tuned configuration -# - -[main] -summary=kernel settings - -[sysctl] -fs.epoll.max_user_watches=785592 -fs.file-max=379724 -kernel.threads-max=29968 -vm.max_map_count=65530 - -[sysfs] -/sys/fs/selinux/avc/cache_threshold=256 -# spectre/meltdown - turn off fix for performance boost -/sys/kernel/debug/x86/pti_enabled=0 -/sys/kernel/debug/x86/retp_enabled=0 -/sys/kernel/debug/x86/ibrs_enabled=0 - -[vm] -transparent_hugepages=never - -[bootloader] -cmdline=spectre_v2=off nopti panic=10001 splash diff --git a/tests/tuned/etc/tuned/bogus/tuned.conf b/tests/tuned/etc/tuned/bogus/tuned.conf deleted file mode 100644 index def0b0e..0000000 --- a/tests/tuned/etc/tuned/bogus/tuned.conf +++ /dev/null @@ -1,2 +0,0 @@ -[bogus section] -bogus=profile diff --git a/tests/tuned/etc/tuned/empty_profile_file/tuned.conf b/tests/tuned/etc/tuned/empty_profile_file/tuned.conf deleted file mode 100644 index e69de29..0000000 diff --git a/tests/tuned/etc/tuned/kernel_settings/tuned.conf b/tests/tuned/etc/tuned/kernel_settings/tuned.conf deleted file mode 100644 index 92c4e7d..0000000 --- a/tests/tuned/etc/tuned/kernel_settings/tuned.conf +++ /dev/null @@ -1,5 +0,0 @@ -# tuned configuration -# - -[main] -summary=kernel settings diff --git a/tests/tuned/etc/tuned/tuned-main.conf b/tests/tuned/etc/tuned/tuned-main.conf deleted file mode 100644 index fd63ade..0000000 --- a/tests/tuned/etc/tuned/tuned-main.conf +++ /dev/null @@ -1,42 +0,0 @@ -#daemon = 0 -#reapply_sysctl = 0 -# Global tuned configuration file. - -# Whether to use daemon. Without daemon it just applies tuning. It is -# not recommended, because many functions don't work without daemon, -# e.g. there will be no D-Bus, no rollback of settings, no hotplug, -# no dynamic tuning, ... -daemon = 1 - -# Dynamicaly tune devices, if disabled only static tuning will be used. -dynamic_tuning = 0 - -# How long to sleep before checking for events (in seconds) -# higher number means lower overhead but longer response time. -sleep_interval = 1 - -# Update interval for dynamic tunings (in seconds). -# It must be multiply of the sleep_interval. -update_interval = 10 - -# Recommend functionality, if disabled "recommend" command will be not -# available in CLI, daemon will not parse recommend.conf but will return -# one hardcoded profile (by default "balanced"). -recommend_command = 1 - -# Whether to reapply sysctl from the e.g /etc/sysctl.conf, /etc/sysctl.d, ... -# If enabled these sysctls will be re-appliead after Tuned sysctls are -# applied, i.e. Tuned sysctls will not override system sysctls. -reapply_sysctl = 1 - -# Default priority assigned to instances -default_instance_priority = 0 - -# Udev buffer size -udev_buffer_size = 1MB - -# Log file count -log_file_count = 2 - -# Log file max size -log_file_max_size = 1MB diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/modules/test_kernel_settings.py b/tests/unit/modules/test_kernel_settings.py deleted file mode 100644 index 08f89da..0000000 --- a/tests/unit/modules/test_kernel_settings.py +++ /dev/null @@ -1,586 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, Rich Megginson -# SPDX-License-Identifier: GPL-2.0-or-later -# -""" Unit tests for kernel_settings module """ - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import os -import tempfile -import shutil -import unittest -import re -import copy -from configobj import ConfigObj - -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock - -import kernel_settings -import tuned - - -class KernelSettingsBootloaderCmdline(unittest.TestCase): - """test bootloader cmdline parsing/formatting/operations""" - - def test_bootloader_cmdline(self): - """do various tests of bootloader_cmdline_add and bootloader_cmdline_remove""" - blcmd = kernel_settings.BLCmdLine("") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine(None) - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("") - blcmd.add("foo", "true") - blcmd.add("bar", None) - blcmd.add("baz", None) - self.assertEqual("foo=true bar baz", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine(None) - blcmd.add("foo", "true") - blcmd.add("bar", None) - blcmd.add("baz", None) - self.assertEqual("foo=true bar baz", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.add("foo", "false") - self.assertEqual("foo=false bar baz", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.add("some", "value") - blcmd.add("another", None) - blcmd.add("value", "has spaces") - blcmd.add("foo", "false") - self.assertEqual( - "foo=false bar baz some=value another value='has spaces'", str(blcmd) - ) - - blcmd = kernel_settings.BLCmdLine("") - blcmd.remove("foo") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine(None) - blcmd.remove("foo") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo") - blcmd.remove("foo") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true") - blcmd.remove("foo") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.remove("foo") - self.assertEqual("bar baz", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.remove("foo") - blcmd.remove("baz") - self.assertEqual("bar", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.remove("baz") - blcmd.remove("bar") - self.assertEqual("foo=true", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.remove("baz") - blcmd.remove("bar") - blcmd.remove("foo") - self.assertEqual("", str(blcmd)) - - blcmd = kernel_settings.BLCmdLine("foo=true bar baz") - blcmd.remove("baz") - blcmd.remove("bar") - blcmd.add("another", None) - blcmd.add("foo", "false") - self.assertEqual("foo=false another", str(blcmd)) - - -class KernelSettingsInputValidation(unittest.TestCase): - """test the code that does the input validation""" - - def assertRegex(self, text, expected_regex, msg=None): - """Fail the test unless the text matches the regular expression.""" - assert re.search(expected_regex, text) - - def test_validate_and_digest_item(self): - """do various tests of validate_and_digest_item""" - item = {} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], "Error: section [bogus] item is missing 'name': {}" - ) - item = {"name": 1} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], "Error: section [bogus] item name [1] is not a valid string" - ) - item = {"name": "name"} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [bogus] item name [name] must have either a " - "'value' or 'state'", - ) - item = {"name": "name"} - errlist = kernel_settings.validate_and_digest_item("bogus", item, True, True) - self.assertEqual(len(errlist), 0) - item = {"name": "name", "value": "value", "state": "state"} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [bogus] item name [name] must have only one of " - "'value' or 'state'", - ) - item = {"name": "name", "state": 0} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [bogus] item name [name] state value must be " - "'absent' not [0]", - ) - item = {"name": "name", "value": ["not a dict"]} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [bogus] item name [name] value [not a dict] is not a dict", - ) - item = {"name": "name", "value": ["not a dict"]} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [bogus] item name [name] value [not a dict] is not a dict", - ) - item = {"name": "name", "value": ["not a dict"]} - errlist = kernel_settings.validate_and_digest_item("bogus", item, False) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - ( - "Error: section [bogus] item [name] has " - "unexpected list value ['not a dict']" - ), - ) - item = {"name": "name", "value": [{"previous": "invalid"}]} - errlist = kernel_settings.validate_and_digest_item("bogus", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - ( - "Error: section [bogus] item name [name] has " - "invalid value for 'previous' [invalid]" - ), - ) - item = {"name": "cmdline", "value": "not a list"} - errlist = kernel_settings.validate_and_digest_item("bootloader", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - ( - "Error: section [bootloader] item [cmdline] must " - "be a list not [not a list]" - ), - ) - item = {"name": "somename", "value": True} - errlist = kernel_settings.validate_and_digest_item("sysctl", item) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - ( - "Error: section [sysctl] item [somename] value [True] " - "must not be a boolean - try quoting the value" - ), - ) - - def test_validate_and_digest(self): - """do various tests of validate_and_digest""" - params = {"section": {"bogus": "dict"}} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - ( - "Error: to remove the section [section] specify " - "the value {'state': 'empty'}" - ), - ) - params = {"section": 0} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], "Error: section [section] value must be a dict or a list" - ) - params = {"section": [0]} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], "Error: section [section] item value [0] is not a dict" - ) - params = {"section": [{"previous": "bogus"}]} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 1) - self.assertEqual( - errlist[0], - "Error: section [section] item has invalid value for 'previous' [bogus]", - ) - params = {"section": [{"name": "name", "value": "value"}]} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - params = {"section": [{"previous": "replaced"}]} - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - self.assertTrue(params["__section_to_replace"]["section"]) - - -class KernelSettingsParamsProfiles(unittest.TestCase): - """test param to profile conversion and vice versa""" - - def assertRegex(self, text, expected_regex, msg=None): - """Fail the test unless the text matches the regular expression.""" - assert re.search(expected_regex, text) - - def setUp(self): - self.test_root_dir = tempfile.mkdtemp(suffix=".lsr") - os.environ["TEST_ROOT_DIR"] = self.test_root_dir - self.test_cleanup = kernel_settings.setup_for_testing() - self.tuned_config = tuned.utils.global_config.GlobalConfig() - self.logger = Mock() - - def tearDown(self): - self.test_cleanup() - shutil.rmtree(self.test_root_dir) - del os.environ["TEST_ROOT_DIR"] - - def test_load_profile(self): - """test the code that loads profiles""" - tuned_app, errmsg = kernel_settings.load_current_profile( - self.tuned_config, "junk", self.logger - ) - self.assertIsNone(tuned_app) - self.assertRegex(errmsg, "Profile does not exist") - tuned_app, errmsg = kernel_settings.load_current_profile( - self.tuned_config, "no_profile_file", self.logger - ) - self.assertIsNone(tuned_app) - self.assertRegex(errmsg, "Profile does not exist") - tuned_app, errmsg = kernel_settings.load_current_profile( - self.tuned_config, "empty_profile_file", self.logger - ) - self.assertIsNone(tuned_app) - self.assertRegex(errmsg, "Error loading tuned profile") - tuned_app, errmsg = kernel_settings.load_current_profile( - self.tuned_config, "bogus", self.logger - ) - self.assertIsNone(tuned_app) - self.assertRegex(errmsg, "Error loading tuned profile") - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "kernel_settings", self.logger - ) - self.assertEqual("kernel settings", tuned_app.daemon.profile.options["summary"]) - self.assertEqual(0, len(tuned_app.daemon.profile.units)) - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "basic_settings", self.logger - ) - self.assertEqual("kernel settings", tuned_app.daemon.profile.options["summary"]) - units = tuned_app.daemon.profile.units - self.assertIn("sysctl", units) - self.assertIn("sysfs", units) - self.assertIn("vm", units) - self.assertIn("bootloader", units) - self.assertEqual(4, len(units["sysctl"].options)) - self.assertEqual(4, len(units["sysfs"].options)) - self.assertEqual(1, len(units["vm"].options)) - self.assertEqual(1, len(units["bootloader"].options)) - - def test_apply_params_to_empty_profile(self): - """test applying params to empty profile""" - params = { - "sysctl": [{"name": "fs.epoll.max_user_watches", "value": 785592}], - "sysfs": [{"name": "/sys/kernel/debug/x86/pti_enabled", "value": 0}], - "vm": [{"name": "transparent_hugepages", "value": "never"}], - "bootloader": [ - { - "name": "cmdline", - "value": [ - {"name": "spectre_v2", "value": "off"}, - {"name": "nopti"}, - {"name": "panic", "value": 10001}, - {"name": "splash"}, - ], - } - ], - } - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "kernel_settings", self.logger - ) - current_profile = tuned_app.daemon.profile - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, False - ) - self.assertEqual(kernel_settings.CHANGES, changestatus) - sysctl = {"fs.epoll.max_user_watches": "785592"} - self.assertEqual(sysctl, dict(current_profile.units["sysctl"].options)) - sysfs = {"/sys/kernel/debug/x86/pti_enabled": "0"} - self.assertEqual(sysfs, dict(current_profile.units["sysfs"].options)) - vm = {"transparent_hugepages": "never"} - self.assertEqual(vm, current_profile.units["vm"].options) - cmdline = {"cmdline": "spectre_v2=off nopti panic=10001 splash"} - self.assertEqual(cmdline, dict(current_profile.units["bootloader"].options)) - self.assertTrue(reboot_required) - # test idempotency - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, False - ) - self.assertEqual(kernel_settings.NOCHANGES, changestatus) - self.assertFalse(reboot_required) - - def test_apply_params_and_ops_to_profile(self): - """test applying params and operations to a profile""" - params = { - "sysctl": [ - {"name": "fs.epoll.max_user_watches", "value": 785592}, - {"name": "kernel.threads-max", "state": "absent"}, - {"name": "fs.file-max", "state": "absent"}, - ], - "sysfs": [ - {"previous": "replaced"}, - {"name": "/sys/kernel/debug/x86/pti_enabled", "value": 0}, - ], - "vm": {"state": "empty"}, - "bootloader": [ - { - "name": "cmdline", - "value": [ - {"name": "someother", "value": "value"}, - {"name": "spectre_v2", "value": "off"}, - {"name": "nopti"}, - {"name": "panic", "state": "absent"}, - {"name": "splash", "state": "absent"}, - ], - } - ], - } - paramsorig = copy.deepcopy(params) - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "basic_settings", self.logger - ) - current_profile = tuned_app.daemon.profile - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, False - ) - self.assertEqual(kernel_settings.CHANGES, changestatus) - sysctl = {"fs.epoll.max_user_watches": "785592", "vm.max_map_count": "65530"} - self.assertEqual(sysctl, dict(current_profile.units["sysctl"].options)) - sysfs = {"/sys/kernel/debug/x86/pti_enabled": "0"} - self.assertEqual(sysfs, dict(current_profile.units["sysfs"].options)) - self.assertNotIn("vm", current_profile.units) - cmdline = {"cmdline": "spectre_v2=off nopti someother=value"} - self.assertEqual(cmdline, dict(current_profile.units["bootloader"].options)) - self.assertTrue(reboot_required) - # idempotency - errlist = kernel_settings.validate_and_digest(paramsorig) - self.assertEqual(len(errlist), 0) - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - paramsorig, current_profile, False - ) - self.assertEqual(kernel_settings.NOCHANGES, changestatus) - self.assertFalse(reboot_required) - - def test_apply_params_with_purge(self): - """test applying params to empty profile""" - params = { - "sysctl": [{"name": "fs.epoll.max_user_watches", "value": 785592}], - "sysfs": [{"name": "/sys/kernel/debug/x86/pti_enabled", "value": 0}], - "bootloader": [ - { - "name": "cmdline", - "value": [ - {"name": "spectre_v2", "value": "off"}, - {"name": "nopti"}, - {"name": "panic", "value": 10001}, - {"name": "splash"}, - ], - } - ], - } - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "basic_settings", self.logger - ) - current_profile = tuned_app.daemon.profile - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, True - ) - self.assertEqual(kernel_settings.CHANGES, changestatus) - sysctl = {"fs.epoll.max_user_watches": "785592"} - self.assertEqual(sysctl, dict(current_profile.units["sysctl"].options)) - sysfs = {"/sys/kernel/debug/x86/pti_enabled": "0"} - self.assertEqual(sysfs, dict(current_profile.units["sysfs"].options)) - self.assertNotIn("vm", current_profile.units) - cmdline = {"cmdline": "spectre_v2=off nopti panic=10001 splash"} - self.assertEqual(cmdline, dict(current_profile.units["bootloader"].options)) - self.assertFalse(reboot_required) - # test idempotency - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, True - ) - self.assertEqual(kernel_settings.NOCHANGES, changestatus) - self.assertFalse(reboot_required) - - def test_write_profile(self): - """test applying params and writing new profile""" - params = { - "sysctl": [{"name": "fs.epoll.max_user_watches", "value": 785592}], - "sysfs": [{"name": "/sys/kernel/debug/x86/pti_enabled", "value": 0}], - "vm": [{"name": "transparent_hugepages", "value": "never"}], - "bootloader": [ - { - "name": "cmdline", - "value": [ - {"name": "spectre_v2", "value": "off"}, - {"name": "nopti"}, - {"name": "panic", "value": 10001}, - {"name": "splash"}, - ], - } - ], - } - errlist = kernel_settings.validate_and_digest(params) - self.assertEqual(len(errlist), 0) - # wokeignore:rule=blacklist - # pylint: disable=blacklisted-name - tuned_app, _ = kernel_settings.load_current_profile( - self.tuned_config, "kernel_settings", self.logger - ) - current_profile = tuned_app.daemon.profile - changestatus, reboot_required = kernel_settings.apply_params_to_profile( - params, current_profile, False - ) - self.assertEqual(kernel_settings.CHANGES, changestatus) - self.assertTrue(reboot_required) - kernel_settings.write_profile( - current_profile, - "# one\n# two\n# three\n", - ) - fname = os.path.join( - tuned.consts.LOAD_DIRECTORIES[-1], "kernel_settings", "tuned.conf" - ) - expected_lines = [ - "# one", - "# two", - "# three", - "[main]", - "summary = kernel settings", - "[sysctl]", - "fs.epoll.max_user_watches = 785592", - "[sysfs]", - "/sys/kernel/debug/x86/pti_enabled = 0", - "[vm]", - "transparent_hugepages = never", - "[bootloader]", - "cmdline = spectre_v2=off nopti panic=10001 splash", - ] - expected = ConfigObj(expected_lines) - actual = None - with open(fname, "r") as ff: - actual = ConfigObj(ff) - self.assertEqual(expected, actual) - self.assertEqual(expected.initial_comment, actual.initial_comment) - - -class KernelSettingsRemoveIfEmpty(unittest.TestCase): - """test remove_if_empty""" - - def test_remove_if_empty(self): - """test remove_if_empty""" - params = {} - self.assertTrue(kernel_settings.remove_if_empty(params)) - self.assertEqual({}, params) - params = {"key1": "", "key2": [], "key3": {}, "key4": None} - self.assertTrue(kernel_settings.remove_if_empty(params)) - self.assertEqual({}, params) - params = ["", [], {}, None] - self.assertTrue(kernel_settings.remove_if_empty(params)) - self.assertEqual([], params) - params = {"key1": "", "key2": [], "key3": {}, "key4": None, "key5": 0} - self.assertFalse(kernel_settings.remove_if_empty(params)) - self.assertEqual({"key5": 0}, params) - params = [1, "", 2, [], 3, {}, 4, None, 5] - self.assertFalse(kernel_settings.remove_if_empty(params)) - self.assertEqual([1, 2, 3, 4, 5], params) - params = 0 - self.assertFalse(kernel_settings.remove_if_empty(params)) - self.assertEqual(0, params) - params = {"key1": [{"key2": [{"key3": 3}]}]} - self.assertFalse(kernel_settings.remove_if_empty(params)) - self.assertEqual({"key1": [{"key2": [{"key3": 3}]}]}, params) - params = {"key1": [{"key2": [{"key3": 3}, {"key4": ""}]}]} - self.assertFalse(kernel_settings.remove_if_empty(params)) - self.assertEqual({"key1": [{"key2": [{"key3": 3}]}]}, params) - params = { - "key11": None, - "key1": [ - "", - {"key2": [{"key3": []}, {"key4": ""}, {"key5": None}, {"key6": {}}]}, - ], - } - self.assertTrue(kernel_settings.remove_if_empty(params)) - self.assertEqual({}, params) - params = [ - None, - {"key11": None}, - { - "key1": [ - "", - { - "key2": [ - {"key3": []}, - {"key4": ""}, - {"key5": None}, - {"key6": {}}, - ] - }, - ] - }, - [], - {}, - "", - ] - self.assertTrue(kernel_settings.remove_if_empty(params)) - self.assertEqual([], params) - - -if __name__ == "__main__": - unittest.main() diff --git a/tox.ini b/tox.ini index b86283c..e0c05c1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,6 @@ # SPDX-License-Identifier: MIT [lsr_config] lsr_enable = true -commands_pre = bash -c '\ - set -euxo pipefail; \ - if [ -f {toxinidir}/tests/install_tuned_for_testing.sh ]; then \ - {toxinidir}/tests/install_tuned_for_testing.sh; \ - elif [ -f {toxinidir}/tests/kernel_settings/install_tuned_for_testing.sh ]; then \ - {toxinidir}/tests/kernel_settings/install_tuned_for_testing.sh; \ - else \ - exit 1; \ - fi' [lsr_ansible-lint] configfile = {toxinidir}/.ansible-lint diff --git a/tuned_requirements.txt b/tuned_requirements.txt deleted file mode 100644 index 998b0a5..0000000 --- a/tuned_requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Note: this requirements file is used to specify what dependencies are -# needed to perform unit testing of tuned and tuned related code. -# To install these pip packages on Fedora, you may also need to install -# some system RPM packages: -# dnf -y install dbus-devel glib2-devel cairo-gobject-devel gobject-introspection-devel -configobj -decorator -dbus-python -PyGObject -pyudev -# note - this is *not* the procfs pip - this is not on pypi -git+https://git.kernel.org/pub/scm/libs/python/python-linux-procfs/python-linux-procfs.git@v0.4.11 ; python_version < "3.0" -git+https://git.kernel.org/pub/scm/libs/python/python-linux-procfs/python-linux-procfs.git@v0.6 ; python_version >= "3.0" diff --git a/vars/main.yml b/vars/main.yml index e535227..818e71d 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -1,11 +1,23 @@ --- __kernel_settings_tuned_profile: kernel_settings -__kernel_settings_profile_src: kernel_settings -__kernel_settings_profile_dir: "/etc/tuned/kernel_settings" +__kernel_settings_profile_src: "{{ __kernel_settings_tuned_profile }}" +__kernel_settings_tuned_dir: /etc/tuned +__kernel_settings_tuned_main_conf_file: >- + {{ __kernel_settings_tuned_dir }}/tuned-main.conf +__kernel_settings_profile_dir: >- + {{ __kernel_settings_profile_parent }}/{{ __kernel_settings_tuned_profile }} __kernel_settings_profile_filename: >- {{ __kernel_settings_profile_dir }}/tuned.conf +__kernel_settings_tuned_profile_mode: >- + {{ __kernel_settings_tuned_dir }}/profile_mode +__kernel_settings_tuned_active_profile: >- + {{ __kernel_settings_tuned_dir }}/active_profile __kernel_settings_state_absent: state: absent +__kernel_settings_state_empty: + state: empty +__kernel_settings_previous_replaced: + previous: replaced # ansible_facts required by the role __kernel_settings_required_facts: