diff --git a/defaults/main.yml b/defaults/main.yml index a8ddc262..a0020ae9 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -25,6 +25,126 @@ kernel_settings_sysctl: [] # value: 0 kernel_settings_sysfs: [] +# This is a `dict` of the settings that fall under tuned's cpu plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and`value`. For example: +# kernel_settings_cpu: +# devices: +# - cpu0 +# cpu1 +# settings: +# - name: governor +# value: powersave +# - name: min_perf_pct +# value: 20 +kernel_settings_cpu: null + +# This is a `dict` of the settings that fall under tuned's disk plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_disk: +# devices: +# settings: +# - name: elevator +# value: bfq +# - name: read_ahead_kb +# value: 256 +# - name: scheduler_quantum +# value: 64 +kernel_settings_disk: null + +# This is a `dict` of the settings that fall under tuned's net plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_net: +# devices: +# settings: +# - name: nf_conntrack_hashsize +# value: 1048576 +kernel_settings_net: null + +# This is a `dict` of the settings that fall under tuned's audio plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_audio: +# devices: +# settings: +# - name: timeout +# value: 10 +# - name: reset_controller +# value: 1 +kernel_settings_audio: null + +# This is a `dict` of the settings that fall under tuned's scsi_host plugin. +# The first item in the dict will be `devices`, which specifies the devices +# (if any) to apply the settings to. The default is to apply to all devices. +# The second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_scsi_host: +# devices: +# settings: +# - name: alpm +# value: "min_power" +kernel_settings_scsi_host: null + +# This is a `dict` of the settings that fall under tuned's video plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_video: +# devices: +# settings: +# - name: radeon_powersave +# value: "auto" +kernel_settings_video: null + +# This is a `dict` of the settings that fall under tuned's usb plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_usb: +# devices: +# settings: +# - name: autosuspend +# value: 1 +kernel_settings_usb: null + +# This is a `dict` of the settings that fall under tuned's selinux plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_selinux: +# devices: +# settings: +# - name: avc_cache_threshold +# value: 180 +kernel_settings_selinux: null + +# This is a `dict` of the settings that fall under tuned's vm plugin. The +# first item in the dict will be `devices`, which specifies the devices (if +# any) to apply the settings to. The default is to apply to all devices. The +# second item in the dict will be `settings`, which consists of a list of +# `dict` items with allowed key names of `name` and `value`. For example: +# kernel_settings_vm: +# devices: +# settings: +# - name: tranmadvise +# - name: tsparent_hugepages +# value: ransparent_hugepages.defrag +# value: madvise +kernel_settings_vm: null + # A space delimited list of cpu numbers. # See systemd-system.conf man page - CPUAffinity kernel_settings_systemd_cpu_affinity: null diff --git a/library/kernel_report.py b/library/kernel_report.py new file mode 100644 index 00000000..026882d0 --- /dev/null +++ b/library/kernel_report.py @@ -0,0 +1,593 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Mary Provencher +# SPDX-License-Identifier: GPL-2.0-or-later +# +""" Generate kernel settings facts for a system """ + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: kernel_report + +short_description: Report kernel settings from a template machine + +version_added: "2.9" + +description: + - | + Report kernel settings from a template machine and adds them to + ansible_facts. The current use case for this module is to be able to + apply kernel settings from a template machine to any number of targets; + particularly when admins may not be aware of which kernel settings they + have tweaked on the template machine and only know that they would like + to apply the kernel state to other machines. This module selectively + loops through sysctl and sysfs kernel settings to generate a "report" on + the kernel state of the template machine. The settings that it loops + through correspond to commonly changed fields in various tuned profiles. + Some of these commonly changed settings are device-specific, meaning that + they are tied to particular devices on the template machine, such as + cpus, usb interfaces, and graphics cards. These device-specific settings + include their corresponding device somewhere in the path name, thus + making it difficult and unreliable to apply directly to a target. To + address this issue, such settings are placed into a separate dictionary + called kernel_settings_device_specific. The dictionaries entitled + kernel_settings_sysfs and kernel_settings_sysctl contain settings which + can be applied directly their corresponding sysfs and sysctl tuned + plugins. The dictionary kernel_settings_other and its children contain + those kernel_settings which correspond to other tuned plugins such as + selinux, cpu, and vm. + - HORIZONTALLINE + - | + Note that if a particular kernel setting file does not exist on the + template system, that setting will not be included in the output of the + report. + +author: + - Mary Provencher (@mprovenc) +""" + +EXAMPLES = """ +# register settings from a template machine +- name: get and store config values from template machine + kernel_report: + register: template_settings +""" + +RETURN = """ +# An example return. +ansible_facts: + description: Facts to add to ansible_facts. + returned: always + type: dict + contains: + kernel_settings_device_specific: + description: Device-specific kernel settings. + type: dict + returned: always + sample: + kernel_settings_cpu_governor: + - device: 'cpu0' + value: null + kernel_settings_disk_elevator: + - device: 'sr0' + value: 'bfq' + kernel_settings_disk_read_ahead_kb: + - device: 'sr0' + value: '128' + - device: 'vda' + value: '128' + kernel_settings_disk_scheduler_quantum: + - device: 'sr0' + value: null + kernel_settings_sampling_down_factor: + - device: 'cpu0' + value: null + kernel_settings_scsi_host_alpm: + - device: 'host0' + value: null + kernel_settings_other: + description: Kernel settings for various tuned plugins. + type: dict + returned: always + sample: + kernel_settings_cpu: + settings: + kernel_settings_net: + settings: + kernel_settings_selinux: + settings: + - name: 'avc_cache_threshold' + value:'512' + kernel_settings_vm: + settings: + - name: 'transparent_hugepage' + value: 'madvise' + - name: 'transparent_hugepage.defrag' + value: 'madvise' + kernel_settings_sysctl: + description: Sysctl kernel settings. + type: dict + returned: always + sample: + - name: 'fs.aio-max-nr' + value: '70000' + - name: 'fs.file-max' + value: '100000' + - name: 'vm.dirty_ratio' + value: '20' + kernel_settings_sysfs: + description: Sysfs settings (no particular tuned plugin) + type: dict + returned: always + sample: + - name: '/sys/kernel/mm/ksm/run' + value: '0' +""" + +import re +import pyudev +from ansible.module_utils.basic import AnsibleModule + +SYSCTL_FIELDS = [ + "fs.aio-max-nr", + "fs.file-max", + "fs.inotify.max_user_watches", + "kernel.hung_task_timeout_secs", + "kernel.nmi_watchdog", + "kernel.numa_balancing", + "kernel.panic_on_oops", + "kernel.pid_max", + "kernel.printk", + "kernel.sched_autogroup_enabled", + "kernel.sched_latency_ns", + "kernel.sched_migration_cost_ns", + "kernel.sched_min_granularity_ns", + "kernel.sched_rt_runtime_us", + "kernel.sched_wakeup_granularity_ns", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.timer_migration", + "net.core.busy_poll", + "net.core.busy_read", + "net.core.rmem_default", + "net.core.rmem_max", + "net.core.wmem_default", + "net.core.wmem_max", + "net.ipv4.ip_local_port_range", + "net.ipv4.tcp_fastopen", + "net.ipv4.tcp_rmem", + "net.ipv4.tcp_timestamps", + "net.ipv4.tcp_window_scaling", + "net.ipv4.tcp_wmem", + "net.ipv4.udp_mem", + "net.netfilter.nf_conntrack_max", + "vm.dirty_background_bytes", + "vm.dirty_background_ratio", + "vm.dirty_bytes", + "vm.dirty_expire_centisecs", + "vm.dirty_ratio", + "vm.dirty_writeback_centisecs", + "vm.hugepages_treat_as_movable", + "vm.laptop_mode", + "vm.max_map_count", + "vm.min_free_kbytes", + "vm.stat_interval", + "vm.swappiness", + "vm.zone_reclaim_mode", +] + + +def safe_file_get_contents(filename): + try: + with open(filename) as f: + return f.read().rstrip() + except IOError as e: + print( + "safe_file_get_contents: suppressed exception FileNotFoundError \ + with content '%s'" + % e + ) + + +def get_sysctl_fields(): + sysctl = [] + for setting in SYSCTL_FIELDS: + temp_dict = {"name": setting} + temp_dict["value"] = safe_file_get_contents( + "/proc/sys/%s" % setting.replace(".", "/") + ) + if temp_dict["value"]: + if ( + setting == "vm.dirty_background_bytes" + or setting == "vm.dirty_background_ratio" + or setting == "vm.dirty_ratio" + or setting == "vm.dirty_bytes" + ) and temp_dict["value"] == "0": + continue + else: + sysctl.append(temp_dict) + + return {"kernel_settings_sysctl": sysctl} + + +def get_sysfs_fields(): + result = {} + + kernel_settings_device_specific = {} + + # these are the settings not specific to any particular device + kernel_settings_other = {} + + kernel_settings_cpu = {"settings": []} + kernel_settings_net = {"settings": []} + kernel_settings_selinux = {"settings": []} + kernel_settings_vm = {"settings": []} + + if safe_file_get_contents("/sys/devices/system/cpu/intel_pstate/min_perf_pct"): + kernel_settings_cpu["settings"].append( + { + "name": "min_perf_pct", + "value": safe_file_get_contents( + "/sys/devices/system/cpu/intel_pstate/min_perf_pct" + ), + } + ) + if safe_file_get_contents("/sys/devices/system/cpu/intel_pstate/max_perf_pct"): + kernel_settings_cpu["settings"].append( + { + "name": "max_perf_pct", + "value": safe_file_get_contents( + "/sys/devices/system/cpu/intel_pstate/max_perf_pct" + ), + } + ) + if safe_file_get_contents("/sys/devices/system/cpu/intel_pstate/no_turbo"): + kernel_settings_cpu["settings"].append( + { + "name": "no_turbo", + "value": safe_file_get_contents( + "/sys/devices/system/cpu/intel_pstate/no_turbo" + ), + } + ) + if safe_file_get_contents("/sys/module/nf_conntrack/parameters/hashsize"): + kernel_settings_net["settings"].append( + { + "name": "nf_conntrack_hashsize", + "value": safe_file_get_contents( + "/sys/module/nf_conntrack/parameters/hashsize" + ), + } + ) + if safe_file_get_contents("/sys/fs/selinux/avc/cache_threshold"): + kernel_settings_selinux["settings"].append( + { + "name": "avc_cache_threshold", + "value": safe_file_get_contents("/sys/fs/selinux/avc/cache_threshold"), + } + ) + if safe_file_get_contents("/sys/kernel/mm/transparent_hugepage/enabled"): + kernel_settings_vm["settings"].append( + { + "name": "transparent_hugepage", + "value": re.findall( + r"\[(\w+)\]", + safe_file_get_contents( + "/sys/kernel/mm/transparent_hugepage/enabled" + ), + )[0], + } + ) + if safe_file_get_contents("/sys/kernel/mm/transparent_hugepage/defrag"): + kernel_settings_vm["settings"].append( + { + "name": "transparent_hugepage.defrag", + "value": re.findall( + r"\[(\w+)\]", + safe_file_get_contents( + "/sys/kernel/mm/transparent_hugepage/defrag" + ), + )[0], + } + ) + + if kernel_settings_cpu["settings"]: + kernel_settings_other["kernel_settings_cpu"] = kernel_settings_cpu + if kernel_settings_net["settings"]: + kernel_settings_other["kernel_settings_net"] = kernel_settings_net + if kernel_settings_selinux["settings"]: + kernel_settings_other["kernel_settings_selinux"] = kernel_settings_selinux + if kernel_settings_vm["settings"]: + kernel_settings_other["kernel_settings_vm"] = kernel_settings_vm + + result["kernel_settings_other"] = kernel_settings_other + + # will collect a list of cpus associated to the template machine, but only use settings from the first one + cpus = pyudev.Context().list_devices(subsystem="cpu") + num_cpus = 0 + + kernel_settings_cpu_governor = [] + kernel_settings_sampling_down_factor = [] + for cpu in cpus: + num_cpus += 1 + kernel_settings_cpu_governor.append( + { + "device": cpu.sys_name, + "value": safe_file_get_contents( + "%s/cpufreq/scaling_governor" % cpu.sys_path + ), + } + ) + kernel_settings_sampling_down_factor.append( + { + "device": cpu.sys_name, + "value": safe_file_get_contents( + "/sys/devices/system/cpu/cpufreq/%s/sampling_down_factor" + % kernel_settings_cpu_governor[-1] + ), + } + ) + + if kernel_settings_cpu_governor: + kernel_settings_device_specific[ + "kernel_settings_cpu_governor" + ] = kernel_settings_cpu_governor + if kernel_settings_sampling_down_factor: + kernel_settings_device_specific[ + "kernel_settings_sampling_down_factor" + ] = kernel_settings_sampling_down_factor + + print( + "get_sysfs_fields: found %d cpus associated to the template machine" % num_cpus + ) + + # will collect a list of blocks associated to the template machine, but only use settings from the first one + blocks = pyudev.Context().list_devices(subsystem="block") + num_blocks = 0 + + kernel_settings_disk_elevator = [] + kernel_settings_disk_read_ahead_kb = [] + kernel_settings_disk_scheduler_quantum = [] + + for block in blocks: + num_blocks += 1 + schedulers = safe_file_get_contents("%s/queue/scheduler" % block.sys_path) + if schedulers: + settings = re.findall(r"\[(\w+)\]", schedulers) + if settings: + kernel_settings_disk_elevator.append( + {"device": block.sys_name, "value": settings[0]} + ) + kernel_settings_disk_read_ahead_kb.append( + { + "device": block.sys_name, + "value": safe_file_get_contents( + "%s/queue/read_ahead_kb" % block.sys_path + ), + } + ) + kernel_settings_disk_scheduler_quantum.append( + { + "device": block.sys_name, + "value": safe_file_get_contents( + "%s/queue/iosched/quantum" % block.sys_path + ), + } + ) + + if kernel_settings_disk_elevator: + kernel_settings_device_specific[ + "kernel_settings_disk_elevator" + ] = kernel_settings_disk_elevator + if kernel_settings_disk_read_ahead_kb: + kernel_settings_device_specific[ + "kernel_settings_disk_read_ahead_kb" + ] = kernel_settings_disk_read_ahead_kb + if kernel_settings_disk_scheduler_quantum: + kernel_settings_device_specific[ + "kernel_settings_disk_scheduler_quantum" + ] = kernel_settings_disk_scheduler_quantum + + print( + "get_sysfs_fields: found %d blocks associated to the template machine" + % num_blocks + ) + + # will collect a list of sound cards associated to the template machine, but only use settings from the first one + num_sound_cards = 0 + sound_cards = ( + pyudev.Context().list_devices(subsystem="sound").match_sys_name("card*") + ) + + kernel_settings_audio_timeout = [] + kernel_settings_audio_reset_controller = [] + + for sound_card in sound_cards: + module_name = sound_card.parent.driver + if module_name in ["snd_hda_intel", "snd_ac97_codec"]: + num_sound_cards += 1 + kernel_settings_audio_timeout.append( + { + "device": sound_card.sys_name, + "value": safe_file_get_contents( + "/sys/module/%s/parameters/power_save" % module_name + ), + } + ) + kernel_settings_audio_reset_controller.append( + { + "device": sound_card.sys_name, + "value": safe_file_get_contents( + "/sys/module/%s/parameters/power_save_controller" % module_name + ), + } + ) + + if kernel_settings_audio_timeout: + kernel_settings_device_specific[ + "kernel_settings_audio_timeout" + ] = kernel_settings_audio_timeout + if kernel_settings_audio_reset_controller: + kernel_settings_device_specific[ + "kernel_settings_audio_reset_controller" + ] = kernel_settings_audio_reset_controller + + print( + "get_sysfs_fields: found %d sound modules associated to the template machine" + % num_sound_cards + ) + + # will collect a list of scsis associated to the template machine, but only use settings from the first one + num_scsis = 0 + scsis = pyudev.Context().list_devices(subsystem="scsi") + + kernel_settings_scsi_host_alpm = [] + + for scsi in scsis: + num_scsis += 1 + kernel_settings_scsi_host_alpm.append( + { + "device": scsi.sys_name, + "value": safe_file_get_contents( + "%s/link_power_management_policy" % scsi.sys_path + ), + } + ) + + if kernel_settings_scsi_host_alpm: + kernel_settings_device_specific[ + "kernel_settings_scsi_host_alpm" + ] = kernel_settings_scsi_host_alpm + print( + "get_sysfs_fields: found %d scsis associated to the template machine" + % num_scsis + ) + + # will collect a list of graphics cards associated to the template machine, but only use settings from the first one + num_gcards = 0 + gcards = ( + pyudev.Context() + .list_devices(subsystem="drm") + .match_sys_name("card*") + .match_property("DEVTYPE", "drm_minor") + ) + + kernel_settings_video_radeon_powersave = [] + + for gcard in gcards: + num_gcards += 1 + method = safe_file_get_contents("%s/device/power_method" % gcard.sys_path) + if method == "profile": + kernel_settings_video_radeon_powersave.append( + { + "device": gcard.sys_name, + "value": safe_file_get_contents( + "%s/device/power_profile" % gcard.sys_path + ), + } + ) + elif method == "dynpm": + kernel_settings_video_radeon_powersave.append( + {"device": gcard.sys_name, "value": "dynpm"} + ) + elif method == "dpm": + kernel_settings_video_radeon_powersave.append( + { + "device": gcard.sys_name, + "value": "dpm-%s" + % safe_file_get_contents( + "%s/device/power_dpm_state" % gcard.sys_path + ), + } + ) + + if kernel_settings_video_radeon_powersave: + kernel_settings_device_specific[ + "kernel_settings_video_radeon_powersave" + ] = kernel_settings_video_radeon_powersave + + print( + "get_sysfs_fields: found %d gcards associated to the template machine" + % num_gcards + ) + + # will collect a list of usb interfaces associated to the template machine, but only use settings from the first one + num_usbs = 0 + usbs = pyudev.Context().list_devices(subsystem="usb") + + kernel_settings_usb_autosuspend = [] + + for usb in usbs: + num_usbs += 1 + kernel_settings_usb_autosuspend.append( + { + "device": usb.sys_name, + "value": safe_file_get_contents("%s/power/autosuspend" % usb.sys_path), + } + ) + + if kernel_settings_usb_autosuspend: + kernel_settings_device_specific[ + "kernel_settings_usb_autosuspend" + ] = kernel_settings_usb_autosuspend + + result["kernel_settings_device_specific"] = kernel_settings_device_specific + + print( + "get_sysfs_fields: found %d usbs associated to the template machine" % num_usbs + ) + + other_sysfs = [] + ksm_dict = {"name": "/sys/kernel/mm/ksm/run"} + ksm_dict["value"] = safe_file_get_contents("/sys/kernel/mm/ksm/run") + if ksm_dict["value"]: + other_sysfs.append(ksm_dict) + + ktimer_dict = {"name": "/sys/kernel/ktimer_lockless_check"} + ktimer_dict["value"] = safe_file_get_contents("/sys/kernel/ktimer_lockless_check") + if ktimer_dict["value"]: + other_sysfs.append(ktimer_dict) + + result["kernel_settings_sysfs"] = other_sysfs + + return result + + +def run_module(): + module_args = dict() + + result = dict( + changed=False, + ansible_facts=dict(), + ) + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) + + if module.check_mode: + module.exit_json(**result) + + result["ansible_facts"] = get_sysfs_fields() + result["ansible_facts"].update(get_sysctl_fields()) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/library/kernel_settings.py b/library/kernel_settings.py index e4d67eaa..8d1a9e9a 100644 --- a/library/kernel_settings.py +++ b/library/kernel_settings.py @@ -307,6 +307,13 @@ def get_supported_tuned_plugin_names(): "sysfs", "systemd", "vm", + "cpu", + "disk", + "net", + "audio", + "scsi_host", + "video", + "usb", ] diff --git a/tasks/main.yml b/tasks/main.yml index c54138e8..dea1e084 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -41,6 +41,47 @@ - name: Apply kernel settings kernel_settings: + cpu: "{{ kernel_settings_cpu is defined | + ternary(kernel_settings_cpu.settings | d([]) | + union([{'name': 'devices', 'value': kernel_settings_cpu.devices | + d('*', true)}]) | d({}), omit) }}" + disk: "{{ kernel_settings_disk is defined | + ternary(kernel_settings_disk.settings | d([]) | + union([{'name': 'devices', \ + 'value': kernel_settings_disk.devices | + d('*', true)}]) | d({}), omit) }}" + net: "{{ kernel_settings_net is defined | + ternary(kernel_settings_net.settings | d([]) | + union([{'name': 'devices', 'value': kernel_settings_net.devices | + d('*', true)}]) | d({}), omit) }}" + audio: "{{ kernel_settings_audio is defined | + ternary(kernel_settings_audio.settings | d([]) | + union([{'name': 'devices', \ + 'value': kernel_settings_audio.devices | + d('*', true)}]) | d({}), omit) }}" + scsi_host: "{{ kernel_settings_scsi_host is defined | + ternary(kernel_settings_scsi_host.settings | d([]) | + union([{'name': 'devices', \ + 'value': kernel_settings_scsi_host.devices | + d('*', true)}]) | d({}), omit) }}" + video: "{{ kernel_settings_video is defined | + ternary(kernel_settings_video.settings | d([]) | + union([{'name': 'devices', \ + 'value': kernel_settings_video.devices | + d('*', true)}]) | d({}), omit) }}" + usb: "{{ kernel_settings_usb is defined | + ternary(kernel_settings_usb.settings | d([]) | + union([{'name': 'devices', 'value': kernel_settings_usb.devices | + d('*', true)}]) | d({}), omit) }}" + selinux: "{{ kernel_settings_selinux is defined | + ternary(kernel_settings_selinux.settings | d([]) | + union([{'name': 'devices', \ + 'value': kernel_settings_selinux.devices | + d('*', true)}]) | d({}), omit) }}" + vm: "{{ kernel_settings_vm is defined | + ternary(kernel_settings_vm.settings | d([]) | + union([{'name': 'devices', 'value': kernel_settings_vm.devices | + d('*', true)}]) | d({}), omit) }}" sysctl: "{{ kernel_settings_sysctl if kernel_settings_sysctl else omit }}" sysfs: "{{ kernel_settings_sysfs if kernel_settings_sysfs else omit }}" systemd: @@ -52,23 +93,6 @@ 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({}) diff --git a/templates/kernel_report_device_specific.j2 b/templates/kernel_report_device_specific.j2 new file mode 100644 index 00000000..9be3f203 --- /dev/null +++ b/templates/kernel_report_device_specific.j2 @@ -0,0 +1 @@ +{{ template_settings.ansible_facts.kernel_settings_device_specific }} \ No newline at end of file diff --git a/tests/tests_apply_kernel_report_settings.yml b/tests/tests_apply_kernel_report_settings.yml new file mode 100644 index 00000000..6bcb753b --- /dev/null +++ b/tests/tests_apply_kernel_report_settings.yml @@ -0,0 +1,116 @@ +- hosts: all + + roles: + - role: linux-system-roles.kernel_settings + when: false + + vars: + names: + - "template" + - "target" + + tasks: + - name: Set version specific variables + include_vars: "{{ lookup('first_found', ffparams) }}" + vars: + ffparams: + files: + - "tests_{{ ansible_distribution }}_\ + {{ ansible_distribution_major_version }}.yml" + - "tests_default.yml" + paths: + - vars + + - name: designate each host as either template or target + set_fact: + host_role: "{{ names[my_idx] }}" + delegate_to: "{{ item }}" + delegate_facts: true + loop: "{{ groups['all'] }}" + loop_control: + index_var: my_idx + run_once: true + + - name: Ensure pyudev package is installed on template machine + package: + name: "{{ __kernel_settings_test_pyudev_pkg }}" + state: present + when: host_role == 'template' + + - name: update sysctl values on the template machine + become: true + sysctl: + name: "{{ item.name }}" + value: "{{ item.value }}" + state: present + reload: yes + loop: "{{ __kernel_settings_test_sysctl }}" + when: host_role == "template" + + - name: get and store config values from template machine + kernel_report: + register: template_settings + when: host_role == 'template' + + - name: create file with device specific settings + template: + src: "/home/mprovenc/linux-system-roles/kernel_settings/\ + templates/kernel_report_device_specific.j2" + dest: "./device_specific_settings.txt" + delegate_to: localhost + when: host_role == 'template' + run_once: true + + - name: set template_hostname fact + set_fact: + template_hostname: "{{ inventory_hostname }}" + loop: "{{ groups['all'] }}" + when: host_role == 'template' + loop_control: + index_var: my_idx + run_once: true + + - name: apply kernel_settings to target machine(s) + include_role: + name: linux-system-roles.kernel_settings + vars: + kernel_settings_cpu: "{{ (hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_other.kernel_settings_cpu) + if 'kernel_settings_cpu' in + hostvars[template_hostname].\ + template_settings.\ + ansible_facts.kernel_settings_other + else omit }}" + kernel_settings_net: "{{ (hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_other.kernel_settings_net) + if 'kernel_settings_net' in + hostvars[template_hostname].\ + template_settings.\ + ansible_facts.kernel_settings_other + else omit }}" + kernel_settings_selinux: "{{ (hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_other.\ + kernel_settings_selinux) + if 'kernel_settings_selinux' in + hostvars[template_hostname].\ + template_settings.\ + ansible_facts.kernel_settings_other + else omit }}" + kernel_settings_vm: "{{ (hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_other.kernel_settings_vm) + if 'kernel_settings_vm' in + hostvars[template_hostname].\ + template_settings.\ + ansible_facts.kernel_settings_other + else omit }}" + kernel_settings_sysfs: "{{ hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_sysfs }}" + kernel_settings_sysctl: "{{ hostvars[template_hostname].\ + template_settings.ansible_facts.\ + kernel_settings_sysctl }}" + when: host_role == 'target' diff --git a/tests/tests_change_settings.yml b/tests/tests_change_settings.yml index cc284a8c..4c944a52 100644 --- a/tests/tests_change_settings.yml +++ b/tests/tests_change_settings.yml @@ -35,6 +35,55 @@ kernel_settings_sysfs: - name: /sys/class/net/lo/mtu value: 65000 + kernel_settings_cpu: + devices: + settings: + - name: governor + value: "conservative|powersave" + - name: min_perf_pct + value: 20 + - name: max_perf_pct + value: 99 + - name: sampling_down_factor + value: 98 + - name: no_turbo + value: 1 + kernel_settings_disk: + devices: + settings: + - name: elevator + value: bfq + - name: read_ahead_kb + value: 256 + - name: scheduler_quantum + value: 64 + kernel_settings_net: + devices: + settings: + - name: nf_conntrack_hashsize + value: 1048576 + kernel_settings_audio: + devices: + settings: + - name: timeout + value: 10 + - name: reset_controller + value: 1 + kernel_settings_scsi_host: + devices: + settings: + - name: alpm + value: "min_power" + kernel_settings_video: + devices: + settings: + - name: radeon_powersave + value: "auto" + kernel_settings_usb: + devices: + settings: + - name: autosuspend + value: 1 - name: check sysfs after role runs command: grep -x 65000 /sys/class/net/lo/mtu diff --git a/tests/unit/modules/test_kernel_report.py b/tests/unit/modules/test_kernel_report.py new file mode 100644 index 00000000..e6e98223 --- /dev/null +++ b/tests/unit/modules/test_kernel_report.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Mary Provencher +# SPDX-License-Identifier: GPL-2.0-or-later +# +""" Unit tests for kernel_report module """ + +import unittest +import sys + +try: + from unittest.mock import patch, mock_open +except ImportError: + from mock import patch, mock_open + +import kernel_report +import pyudev + + +def dirty_sysctl_side_effect(*args, **kwargs): + if ( + args[0] == "/proc/sys/vm/dirty_background_bytes" + or args[0] == "/proc/sys/vm/dirty_background_ratio" + or args[0] == "/proc/sys/vm/dirty_bytes" + or args[0] == "/proc/sys/vm/dirty_ratio" + ): + return "0" + return None + + +def regex_side_effect(*args, **kwargs): + if ( + args[0] == "/sys/kernel/mm/transparent_hugepage/enabled" + or args[0] == "/sys/kernel/mm/transparent_hugepage/defrag" + ): + return "always defer defer+madvise [madvise] never" + return None + + +class KernelReportGetFields(unittest.TestCase): + """test operations involving getting existing values from a template machine""" + + if sys.version_info.major == 3: + builtin_module_name = "builtins" + else: + builtin_module_name = "__builtin__" + + @patch("{}.open".format(builtin_module_name), new_callable=mock_open, read_data="1") + def test_safe_file_get_contents_fails_nonexist_file(self, mock_open): + mock_open.side_effect = IOError + rtn = kernel_report.safe_file_get_contents("junk") + self.assertIsNone(rtn) + + @patch("kernel_report.safe_file_get_contents") + def test_get_sysctl_dirty_values_not_zero(self, mock_get_file): + """do various tests of get_sysctl_fields""" + mock_get_file.side_effect = dirty_sysctl_side_effect + sysctl = kernel_report.get_sysctl_fields() + self.assertEqual([], sysctl["kernel_settings_sysctl"]) + + @patch("kernel_report.safe_file_get_contents") + def test_get_sysctl_no_none_values(self, mock_get_file): + """do various tests of get_sysctl_fields""" + mock_get_file.return_value = None + sysctl = kernel_report.get_sysctl_fields() + self.assertEqual([], sysctl["kernel_settings_sysctl"]) + + @patch("kernel_report.safe_file_get_contents") + def test_get_sysfs_other_settings_empty(self, mock_get_file): + """test for kernel_settings_other to be empty when the files don't exist""" + mock_get_file.return_value = None + sysfs = kernel_report.get_sysfs_fields() + self.assertEqual({}, sysfs["kernel_settings_other"]) + + @patch("kernel_report.safe_file_get_contents") + def test_get_sysfs_regex_fields(self, mock_get_file): + """test that regex is successful for transparent/defrag fields""" + mock_get_file.side_effect = regex_side_effect + sysfs = kernel_report.get_sysfs_fields() + for setting in sysfs["kernel_settings_other"]["kernel_settings_vm"]["settings"]: + if ( + setting["name"] == "transparent_hugepage" + or setting["name"] == "transparent_hugepage.defrag" + ): + self.assertEqual("madvise", setting["value"]) + + @patch("kernel_report.pyudev.Context.list_devices") + def test_get_sysfs_no_devices(self, mock_list_devices): + """ensure that when no devices exist kernel_settings_device_specific is empty""" + mock_list_devices.return_value = pyudev.Context().list_devices(subsystem="") + sysfs = kernel_report.get_sysfs_fields() + self.assertEqual({}, sysfs["kernel_settings_device_specific"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/vars/tests_CentOS_8.yml b/tests/vars/tests_CentOS_8.yml index f6f5e9f4..0edfe629 100644 --- a/tests/vars/tests_CentOS_8.yml +++ b/tests/vars/tests_CentOS_8.yml @@ -1,2 +1,3 @@ __kernel_settings_test_python_pkgs: ['python3', 'python3-configobj'] __kernel_settings_test_python_cmd: python3 +__kernel_settings_test_pyudev_pkg: 'python3-pyudev' diff --git a/tests/vars/tests_RedHat_8.yml b/tests/vars/tests_RedHat_8.yml index f6f5e9f4..0edfe629 100644 --- a/tests/vars/tests_RedHat_8.yml +++ b/tests/vars/tests_RedHat_8.yml @@ -1,2 +1,3 @@ __kernel_settings_test_python_pkgs: ['python3', 'python3-configobj'] __kernel_settings_test_python_cmd: python3 +__kernel_settings_test_pyudev_pkg: 'python3-pyudev' diff --git a/tests/vars/tests_default.yml b/tests/vars/tests_default.yml index b6b6c160..768a4e67 100644 --- a/tests/vars/tests_default.yml +++ b/tests/vars/tests_default.yml @@ -1,2 +1,16 @@ __kernel_settings_test_python_pkgs: ['python', 'python-configobj'] __kernel_settings_test_python_cmd: python +__kernel_settings_test_pyudev_pkg: 'python-pyudev' +__kernel_settings_test_sysctl: + - name: fs.file-max + value: '99999' + - name: fs.aio-max-nr + value: '70000' + - name: kernel.sched_migration_cost_ns + value: '5000000' + - name: kernel.sched_wakeup_granularity_ns + value: '900000' + - name: vm.dirty_expire_centisecs + value: '2000' + - name: kernel.panic_on_oops + value: '1'