From 453069547c8b0ad55e5e627e002b588860efbe02 Mon Sep 17 00:00:00 2001 From: eabdullin Date: Wed, 28 Jan 2026 11:58:05 +0000 Subject: [PATCH] Import from CS git --- SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch | 790 ++++++++++++++++ ...-2-fence_nutanix_ahv-update-metadata.patch | 39 + ...7-fix-bundled-urllib3-CVE-2025-66418.patch | 68 ++ ...6-fix-bundled-urllib3-CVE-2025-66471.patch | 845 ++++++++++++++++++ ...3-fix-bundled-urllib3-CVE-2026-21441.patch | 44 + ...47-fix-bundled-pyasn1-CVE-2026-23490.patch | 28 + SPECS/fence-agents.spec | 46 +- 7 files changed, 1858 insertions(+), 2 deletions(-) create mode 100644 SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch create mode 100644 SOURCES/RHEL-110964-2-fence_nutanix_ahv-update-metadata.patch create mode 100644 SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch create mode 100644 SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch create mode 100644 SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch create mode 100644 SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch diff --git a/SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch b/SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch new file mode 100644 index 0000000..15509f2 --- /dev/null +++ b/SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch @@ -0,0 +1,790 @@ +--- a/agents/nutanix_ahv/fence_nutanix_ahv.py 1970-01-01 01:00:00.000000000 +0100 ++++ b/agents/nutanix_ahv/fence_nutanix_ahv.py 2025-02-25 16:27:56.973414013 +0100 +@@ -0,0 +1,583 @@ ++#!@PYTHON@ -tt ++ ++# AHV Fence agent ++# Compatible with Nutanix v4 API ++ ++ ++import atexit ++import logging ++import sys ++import time ++import uuid ++import requests ++from requests.adapters import HTTPAdapter ++from requests.packages.urllib3.util.retry import Retry ++ ++sys.path.append("@FENCEAGENTSLIBDIR@") ++from fencing import * ++from fencing import fail, EC_LOGIN_DENIED, EC_GENERIC_ERROR, EC_TIMED_OUT, run_delay, EC_BAD_ARGS ++ ++ ++V4_VERSION = '4.0' ++MIN_TIMEOUT = 60 ++PC_PORT = 9440 ++POWER_STATES = {"ON": "on", "OFF": "off", "PAUSED": "off", "UNKNOWN": "unknown"} ++MAX_RETRIES = 5 ++ ++ ++class NutanixClientException(Exception): ++ pass ++ ++ ++class AHVFenceAgentException(Exception): ++ pass ++ ++ ++class TaskTimedOutException(Exception): ++ pass ++ ++ ++class InvalidArgsException(Exception): ++ pass ++ ++ ++class NutanixClient: ++ def __init__(self, username, password, disable_warnings=False): ++ self.username = username ++ self.password = password ++ self.valid_status_codes = [200, 202] ++ self.disable_warnings = disable_warnings ++ self.session = requests.Session() ++ self.session.auth = (self.username, self.password) ++ ++ retry_strategy = Retry(total=MAX_RETRIES, ++ backoff_factor=1, ++ status_forcelist=[429, 500, 503]) ++ ++ self.session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) ++ ++ def request(self, url, method='GET', headers=None, **kwargs): ++ ++ if self.disable_warnings: ++ requests.packages.urllib3.disable_warnings() ++ ++ if headers: ++ self.session.headers.update(headers) ++ ++ response = None ++ ++ try: ++ logging.debug("Sending %s request to %s", method, url) ++ response = self.session.request(method, url, **kwargs) ++ response.raise_for_status() ++ except requests.exceptions.SSLError as err: ++ logging.error("Secure connection failed, verify SSL certificate") ++ logging.error("Error message: %s", err) ++ raise NutanixClientException("Secure connection failed") from err ++ except requests.exceptions.RequestException as err: ++ logging.error("API call failed: %s", response.text) ++ logging.error("Error message: %s", err) ++ raise NutanixClientException(f"API call failed: {err}") from err ++ except Exception as err: ++ logging.error("API call failed: %s", response.text) ++ logging.error("Unknown error %s", err) ++ raise NutanixClientException(f"API call failed: {err}") from err ++ ++ if response.status_code not in self.valid_status_codes: ++ logging.error("API call returned status code %s", response.status_code) ++ raise NutanixClientException(f"API call failed: {response}") ++ ++ return response ++ ++ ++class NutanixV4Client(NutanixClient): ++ def __init__(self, host=None, username=None, password=None, ++ verify=True, disable_warnings=False): ++ self.host = host ++ self.username = username ++ self.password = password ++ self.verify = verify ++ self.base_url = f"https://{self.host}:{PC_PORT}/api" ++ self.vm_url = f"{self.base_url}/vmm/v{V4_VERSION}/ahv/config/vms" ++ self.task_url = f"{self.base_url}/prism/v{V4_VERSION}/config/tasks" ++ super().__init__(username, password, disable_warnings) ++ ++ def _get_headers(self, vm_uuid=None): ++ resp = None ++ headers = {'Accept':'application/json', ++ 'Content-Type': 'application/json'} ++ ++ if vm_uuid: ++ try: ++ resp = self._get_vm(vm_uuid) ++ except AHVFenceAgentException as err: ++ logging.error("Unable to retrieve etag") ++ raise AHVFenceAgentException from err ++ ++ etag_str = resp.headers['Etag'] ++ request_id = str(uuid.uuid1()) ++ headers['If-Match'] = etag_str ++ headers['Ntnx-Request-Id'] = request_id ++ ++ return headers ++ ++ def _get_all_vms(self, filter_str=None, limit=None): ++ vm_url = self.vm_url ++ ++ if filter_str and limit: ++ vm_url = f"{vm_url}?$filter={filter_str}&$limit={limit}" ++ elif filter_str and not limit: ++ vm_url = f"{vm_url}?$filter={filter_str}" ++ elif limit and not filter_str: ++ vm_url = f"{vm_url}?$limit={limit}" ++ ++ logging.debug("Getting info for all VMs, %s", vm_url) ++ header_str = self._get_headers() ++ ++ try: ++ resp = self.request(url=vm_url, method='GET', ++ headers=header_str, verify=self.verify) ++ except NutanixClientException as err: ++ logging.error("Unable to retrieve VM info") ++ raise AHVFenceAgentException from err ++ ++ vms = resp.json() ++ return vms ++ ++ def _get_vm_uuid(self, vm_name): ++ vm_uuid = None ++ resp = None ++ ++ if not vm_name: ++ logging.error("VM name was not provided") ++ raise AHVFenceAgentException("VM name not provided") ++ ++ try: ++ filter_str = f"name eq '{vm_name}'" ++ resp = self._get_all_vms(filter_str=filter_str) ++ except AHVFenceAgentException as err: ++ logging.error("Failed to get VM info for VM %s", vm_name) ++ raise AHVFenceAgentException from err ++ ++ if not resp or not isinstance(resp, dict): ++ logging.error("Failed to retrieve VM UUID for VM %s", vm_name) ++ raise AHVFenceAgentException(f"Failed to get VM UUID for {vm_name}") ++ ++ if 'data' not in resp: ++ err = f"Error: Unsuccessful match for VM name: {vm_name}" ++ logging.error("Failed to retrieve VM UUID for VM %s", vm_name) ++ raise AHVFenceAgentException(err) ++ ++ for vm in resp['data']: ++ if vm['name'] == vm_name: ++ vm_uuid = vm['extId'] ++ break ++ ++ return vm_uuid ++ ++ def _get_vm(self, vm_uuid): ++ if not vm_uuid: ++ logging.error("VM UUID was not provided") ++ raise AHVFenceAgentException("VM UUID not provided") ++ ++ vm_url = self.vm_url + f"/{vm_uuid}" ++ logging.debug("Getting config information for VM, %s", vm_uuid) ++ ++ try: ++ header_str = self._get_headers() ++ resp = self.request(url=vm_url, method='GET', ++ headers=header_str, verify=self.verify) ++ except NutanixClientException as err: ++ logging.error("Failed to retrieve VM details " ++ "for VM UUID: %s", vm_uuid) ++ raise AHVFenceAgentException from err ++ except AHVFenceAgentException as err: ++ logging.error("Failed to retrieve etag from headers") ++ raise AHVFenceAgentException from err ++ ++ return resp ++ ++ def _power_on_off_vm(self, power_state=None, vm_uuid=None): ++ resp = None ++ vm_url = None ++ ++ if not vm_uuid: ++ logging.error("VM UUID was not provided") ++ raise AHVFenceAgentException("VM UUID not provided") ++ if not power_state: ++ logging.error("Requested VM power state is None") ++ raise InvalidArgsException ++ ++ power_state = power_state.lower() ++ ++ if power_state == 'on': ++ vm_url = self.vm_url + f"/{vm_uuid}/$actions/power-on" ++ logging.debug("Sending request to power on VM, %s", vm_uuid) ++ elif power_state == 'off': ++ vm_url = self.vm_url + f"/{vm_uuid}/$actions/power-off" ++ logging.debug("Sending request to power off VM, %s", vm_uuid) ++ else: ++ logging.error("Invalid power state specified: %s", power_state) ++ raise InvalidArgsException ++ ++ try: ++ headers_str = self._get_headers(vm_uuid) ++ resp = self.request(url=vm_url, method='POST', ++ headers=headers_str, verify=self.verify) ++ except NutanixClientException as err: ++ logging.error("Failed to power off VM %s", vm_uuid) ++ raise AHVFenceAgentException from err ++ except AHVFenceAgentException as err: ++ logging.error("Failed to retrieve etag from headers") ++ raise AHVFenceAgentException from err ++ ++ return resp ++ ++ def _power_cycle_vm(self, vm_uuid): ++ if not vm_uuid: ++ logging.error("VM UUID was not provided") ++ raise AHVFenceAgentException("VM UUID not provided") ++ ++ resp = None ++ vm_url = self.vm_url + f"/{vm_uuid}/$actions/power-cycle" ++ logging.debug("Sending request to power cycle VM, %s", vm_uuid) ++ ++ try: ++ header_str = self._get_headers(vm_uuid) ++ resp = self.request(url=vm_url, method='POST', ++ headers=header_str, verify=self.verify) ++ except NutanixClientException as err: ++ logging.error("Failed to power on VM %s", vm_uuid) ++ raise AHVFenceAgentException from err ++ except AHVFenceAgentException as err: ++ logging.error("Failed to retrieve etag from headers") ++ raise AHVFenceAgentException from err ++ ++ return resp ++ ++ def _wait_for_task(self, task_uuid, timeout=None): ++ if not task_uuid: ++ logging.error("Task UUID was not provided") ++ raise AHVFenceAgentException("Task UUID not provided") ++ ++ task_url = f"{self.task_url}/{task_uuid}" ++ header_str = self._get_headers() ++ task_resp = None ++ interval = 5 ++ task_status = None ++ ++ if not timeout: ++ timeout = MIN_TIMEOUT ++ else: ++ try: ++ timeout = int(timeout) ++ except ValueError: ++ timeout = MIN_TIMEOUT ++ ++ while task_status != 'SUCCEEDED': ++ if timeout <= 0: ++ raise TaskTimedOutException(f"Task timed out: {task_uuid}") ++ ++ time.sleep(interval) ++ timeout = timeout - interval ++ ++ try: ++ task_resp = self.request(url=task_url, method='GET', ++ headers=header_str, verify=self.verify) ++ task_status = task_resp.json()['data']['status'] ++ except NutanixClientException as err: ++ logging.error("Unable to retrieve task status") ++ raise AHVFenceAgentException from err ++ except Exception as err: ++ logging.error("Unknown error") ++ raise AHVFenceAgentException from err ++ ++ if task_status == 'FAILED': ++ raise AHVFenceAgentException(f"Task failed, task uuid: {task_uuid}") ++ ++ def list_vms(self, filter_str=None, limit=None): ++ vms = None ++ vm_list = {} ++ ++ try: ++ vms = self._get_all_vms(filter_str, limit) ++ except NutanixClientException as err: ++ logging.error("Failed to retrieve VM list") ++ raise AHVFenceAgentException from err ++ ++ if not vms or not isinstance(vms, dict): ++ logging.error("Failed to retrieve VM list") ++ raise AHVFenceAgentException("Unable to get VM list") ++ ++ if 'data' not in vms: ++ err = "Got invalid or empty VM list" ++ logging.debug(err) ++ else: ++ for vm in vms['data']: ++ vm_name = vm['name'] ++ ext_id = vm['extId'] ++ power_state = vm['powerState'] ++ vm_list[vm_name] = (ext_id, power_state) ++ ++ return vm_list ++ ++ def get_power_state(self, vm_name=None, vm_uuid=None): ++ resp = None ++ power_state = None ++ ++ if not vm_name and not vm_uuid: ++ logging.error("Require at least one of VM name or VM UUID") ++ raise InvalidArgsException("No arguments provided") ++ ++ if not vm_uuid: ++ try: ++ vm_uuid = self._get_vm_uuid(vm_name) ++ except AHVFenceAgentException as err: ++ logging.error("Unable to retrieve UUID of VM, %s", vm_name) ++ raise AHVFenceAgentException from err ++ ++ try: ++ resp = self._get_vm(vm_uuid) ++ except AHVFenceAgentException as err: ++ logging.error("Unable to retrieve power state of VM %s", vm_uuid) ++ raise AHVFenceAgentException from err ++ ++ try: ++ power_state = resp.json()['data']['powerState'] ++ except AHVFenceAgentException as err: ++ logging.error("Failed to retrieve power state of VM %s", vm_uuid) ++ raise AHVFenceAgentException from err ++ ++ return POWER_STATES[power_state] ++ ++ def set_power_state(self, vm_name=None, vm_uuid=None, ++ power_state='off', timeout=None): ++ resp = None ++ current_power_state = None ++ power_state = power_state.lower() ++ ++ if not timeout: ++ timeout = MIN_TIMEOUT ++ ++ if not vm_name and not vm_uuid: ++ logging.error("Require at least one of VM name or VM UUID") ++ raise InvalidArgsException("No arguments provided") ++ ++ if not vm_uuid: ++ vm_uuid = self._get_vm_uuid(vm_name) ++ ++ try: ++ current_power_state = self.get_power_state(vm_uuid=vm_uuid) ++ except AHVFenceAgentException as err: ++ raise AHVFenceAgentException from err ++ ++ if current_power_state.lower() == power_state.lower(): ++ logging.debug("VM already powered %s", power_state.lower()) ++ return ++ ++ if power_state.lower() == 'on': ++ resp = self._power_on_off_vm(power_state, vm_uuid) ++ elif power_state.lower() == 'off': ++ resp = self._power_on_off_vm(power_state, vm_uuid) ++ ++ task_id = resp.json()['data']['extId'] ++ ++ try: ++ self._wait_for_task(task_id, timeout) ++ except AHVFenceAgentException as err: ++ logging.error("Failed to power %s VM", power_state.lower()) ++ logging.error("VM power %s task failed", power_state.lower()) ++ raise AHVFenceAgentException from err ++ except TaskTimedOutException as err: ++ logging.error("Timed out powering %s VM %s", ++ power_state.lower(), vm_uuid) ++ raise TaskTimedOutException from err ++ ++ logging.debug("Powered %s VM, %s successfully", ++ power_state.lower(), vm_uuid) ++ ++ def power_cycle_vm(self, vm_name=None, vm_uuid=None, timeout=None): ++ resp = None ++ status = None ++ ++ if not timeout: ++ timeout = MIN_TIMEOUT ++ ++ if not vm_name and not vm_uuid: ++ logging.error("Require at least one of VM name or VM UUID") ++ raise InvalidArgsException("No arguments provided") ++ ++ if not vm_uuid: ++ vm_uuid = self._get_vm_uuid(vm_name) ++ ++ resp = self._power_cycle_vm(vm_uuid) ++ task_id = resp.json()['data']['extId'] ++ ++ try: ++ self._wait_for_task(task_id, timeout) ++ except AHVFenceAgentException as err: ++ logging.error("Failed to power-cycle VM %s", vm_uuid) ++ logging.error("VM power-cycle task failed with status, %s", status) ++ raise AHVFenceAgentException from err ++ except TaskTimedOutException as err: ++ logging.error("Timed out power-cycling VM %s", vm_uuid) ++ raise TaskTimedOutException from err ++ ++ ++ logging.debug("Power-cycled VM, %s", vm_uuid) ++ ++ ++def connect(options): ++ host = options["--ip"] ++ username = options["--username"] ++ password = options["--password"] ++ verify_ssl = True ++ disable_warnings = False ++ ++ if "--ssl-insecure" in options: ++ verify_ssl = False ++ disable_warnings = True ++ ++ client = NutanixV4Client(host, username, password, ++ verify_ssl, disable_warnings) ++ ++ try: ++ client.list_vms(limit=1) ++ except AHVFenceAgentException as err: ++ logging.error("Connection to Prism Central Failed") ++ logging.error(err) ++ fail(EC_LOGIN_DENIED) ++ ++ return client ++ ++def get_list(client, options): ++ vm_list = None ++ ++ filter_str = options.get("--filter", None) ++ limit = options.get("--limit", None) ++ ++ try: ++ vm_list = client.list_vms(filter_str, limit) ++ except AHVFenceAgentException as err: ++ logging.error("Failed to list VMs") ++ logging.error(err) ++ fail(EC_GENERIC_ERROR) ++ ++ return vm_list ++ ++def get_power_status(client, options): ++ vmid = None ++ name = None ++ power_state = None ++ ++ vmid = options.get("--uuid", None) ++ name = options.get("--plug", None) ++ ++ if not vmid and not name: ++ logging.error("Need VM name or VM UUID for power op") ++ fail(EC_BAD_ARGS) ++ try: ++ power_state = client.get_power_state(vm_name=name, vm_uuid=vmid) ++ except AHVFenceAgentException: ++ fail(EC_GENERIC_ERROR) ++ except InvalidArgsException: ++ fail(EC_BAD_ARGS) ++ ++ return power_state ++ ++def set_power_status(client, options): ++ action = options["--action"].lower() ++ timeout = options.get("--power-timeout", None) ++ vmid = options.get("--uuid", None) ++ name = options.get("--plug", None) ++ ++ if not name and not vmid: ++ logging.error("Need VM name or VM UUID to set power state of a VM") ++ fail(EC_BAD_ARGS) ++ ++ try: ++ client.set_power_state(vm_name=name, vm_uuid=vmid, ++ power_state=action, timeout=timeout) ++ except AHVFenceAgentException as err: ++ logging.error(err) ++ fail(EC_GENERIC_ERROR) ++ except TaskTimedOutException as err: ++ logging.error(err) ++ fail(EC_TIMED_OUT) ++ except InvalidArgsException: ++ fail(EC_BAD_ARGS) ++ ++def power_cycle(client, options): ++ timeout = options.get("--power-timeout", None) ++ vmid = options.get("--uuid", None) ++ name = options.get("--plug", None) ++ ++ if not name and not vmid: ++ logging.error("Need VM name or VM UUID to set power cycling a VM") ++ fail(EC_BAD_ARGS) ++ ++ try: ++ client.power_cycle_vm(vm_name=name, vm_uuid=vmid, timeout=timeout) ++ except AHVFenceAgentException as err: ++ logging.error(err) ++ fail(EC_GENERIC_ERROR) ++ except TaskTimedOutException as err: ++ logging.error(err) ++ fail(EC_TIMED_OUT) ++ except InvalidArgsException: ++ fail(EC_BAD_ARGS) ++ ++def define_new_opts(): ++ all_opt["filter"] = { ++ "getopt": ":", ++ "longopt": "filter", ++ "help": """ ++ --filter=[filter] Filter list, list VMs actions. ++ --filter=\"name eq 'node1-vm'\" ++ --filter=\"startswith(name,'node')\" ++ --filter=\"name in ('node1-vm','node-3-vm')\" """, ++ "required": "0", ++ "shortdesc": "Filter list, get_list" ++ "e.g: \"name eq 'node1-vm'\"", ++ "order": 2 ++ } ++ ++def main(): ++ device_opt = [ ++ "ipaddr", ++ "login", ++ "passwd", ++ "ssl", ++ "notls", ++ "web", ++ "port", ++ "filter", ++ "method", ++ "disable_timeout", ++ "power_timeout" ++ ] ++ ++ atexit.register(atexit_handler) ++ define_new_opts() ++ ++ all_opt["power_timeout"]["default"] = str(MIN_TIMEOUT) ++ options = check_input(device_opt, process_input(device_opt)) ++ docs = {} ++ docs["shortdesc"] = "Fencing agent for Nutanix AHV Cluster VMs." ++ docs["longdesc"] = """fence_ahv is a power fencing agent for \ ++virtual machines deployed on Nutanix AHV cluster with the AHV cluster \ ++being managed by Prism Central.""" ++ docs["vendorurl"] = "https://www.nutanix.com" ++ show_docs(options, docs) ++ run_delay(options) ++ client = connect(options) ++ ++ result = fence_action(client, options, set_power_status, get_power_status, ++ get_list, reboot_cycle_fn=power_cycle ++ ) ++ ++ sys.exit(result) ++ ++ ++if __name__ == "__main__": ++ main() +--- a/tests/data/metadata/fence_nutanix_ahv.xml 1970-01-01 01:00:00.000000000 +0100 ++++ b/tests/data/metadata/fence_nutanix_ahv.xml 2025-02-25 16:27:56.959413680 +0100 +@@ -0,0 +1,201 @@ ++ ++ ++fence_ahv is a power fencing agent for virtual machines deployed on Nutanix AHV cluster with the AHV cluster being managed by Prism Central. ++https://www.nutanix.com ++ ++ ++ ++ ++ Fencing action ++ ++ ++ ++ ++ IP address or hostname of fencing device ++ ++ ++ ++ ++ IP address or hostname of fencing device ++ ++ ++ ++ ++ TCP/UDP port to use for connection with device ++ ++ ++ ++ ++ Login name ++ ++ ++ ++ ++ ++ Method to fence ++ ++ ++ ++ ++ Disable TLS negotiation and force SSL3.0. This should only be used for devices that do not support TLS1.0 and up. ++ ++ ++ ++ ++ Login password or passphrase ++ ++ ++ ++ ++ Script to run to retrieve password ++ ++ ++ ++ ++ Login password or passphrase ++ ++ ++ ++ ++ Script to run to retrieve password ++ ++ ++ ++ ++ Physical plug number on device, UUID or identification of machine ++ ++ ++ ++ ++ Physical plug number on device, UUID or identification of machine ++ ++ ++ ++ ++ Use SSL connection with verifying certificate ++ ++ ++ ++ ++ Use SSL connection without verifying certificate ++ ++ ++ ++ ++ Use SSL connection with verifying certificate ++ ++ ++ ++ ++ Login name ++ ++ ++ ++ ++ Filter list, get_liste.g: "name eq 'node1-vm'" ++ ++ ++ ++ ++ Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. ++ ++ ++ ++ ++ Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity. ++ ++ ++ ++ ++ Level of debugging detail in output. Defaults to the number of --verbose flags specified on the command line, or to 1 if verbose=1 in a stonith device configuration (i.e., on stdin). ++ ++ ++ ++ ++ Write debug information to given file ++ ++ ++ ++ Write debug information to given file ++ ++ ++ ++ ++ Display version information and exit ++ ++ ++ ++ ++ Display help and exit ++ ++ ++ ++ ++ Separator for plug parameter when specifying more than 1 plug ++ ++ ++ ++ ++ Separator for CSV created by 'list' operation ++ ++ ++ ++ ++ Wait X seconds before fencing is started ++ ++ ++ ++ ++ Disable timeout (true/false) (default: true when run from Pacemaker 2.0+) ++ ++ ++ ++ ++ Wait X seconds for cmd prompt after login ++ ++ ++ ++ ++ Test X seconds for status change after ON/OFF ++ ++ ++ ++ ++ Wait X seconds after issuing ON/OFF ++ ++ ++ ++ ++ Wait X seconds for cmd prompt after issuing command ++ ++ ++ ++ ++ Sleep X seconds between status calls during a STONITH action ++ ++ ++ ++ ++ Count of attempts to retry power on ++ ++ ++ ++ Path to gnutls-cli binary ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/SOURCES/RHEL-110964-2-fence_nutanix_ahv-update-metadata.patch b/SOURCES/RHEL-110964-2-fence_nutanix_ahv-update-metadata.patch new file mode 100644 index 0000000..ccd6867 --- /dev/null +++ b/SOURCES/RHEL-110964-2-fence_nutanix_ahv-update-metadata.patch @@ -0,0 +1,39 @@ +From 345a609a34ea153e168b15d48eb8f2dd8d017f04 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Wed, 23 Apr 2025 15:25:13 +0200 +Subject: [PATCH] fence_nutanix_ahv: update metadata to fix incorrect agent + name and align with other agents metadata + +--- + agents/nutanix_ahv/fence_nutanix_ahv.py | 4 ++-- + tests/data/metadata/fence_nutanix_ahv.xml | 4 ++-- + 2 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/agents/nutanix_ahv/fence_nutanix_ahv.py b/agents/nutanix_ahv/fence_nutanix_ahv.py +index 67e6d907c..c18d6a46e 100644 +--- a/agents/nutanix_ahv/fence_nutanix_ahv.py ++++ b/agents/nutanix_ahv/fence_nutanix_ahv.py +@@ -563,8 +563,8 @@ def main(): + all_opt["power_timeout"]["default"] = str(MIN_TIMEOUT) + options = check_input(device_opt, process_input(device_opt)) + docs = {} +- docs["shortdesc"] = "Fencing agent for Nutanix AHV Cluster VMs." +- docs["longdesc"] = """fence_ahv is a power fencing agent for \ ++ docs["shortdesc"] = "Fence agent for Nutanix AHV Cluster VMs." ++ docs["longdesc"] = """fence_nutanix_ahv is a Power Fencing agent for \ + virtual machines deployed on Nutanix AHV cluster with the AHV cluster \ + being managed by Prism Central.""" + docs["vendorurl"] = "https://www.nutanix.com" +diff --git a/tests/data/metadata/fence_nutanix_ahv.xml b/tests/data/metadata/fence_nutanix_ahv.xml +index bbc307b1d..2de4b01ad 100644 +--- a/tests/data/metadata/fence_nutanix_ahv.xml ++++ b/tests/data/metadata/fence_nutanix_ahv.xml +@@ -1,6 +1,6 @@ + +- +-fence_ahv is a power fencing agent for virtual machines deployed on Nutanix AHV cluster with the AHV cluster being managed by Prism Central. ++ ++fence_nutanix_ahv is a Power Fencing agent for virtual machines deployed on Nutanix AHV cluster with the AHV cluster being managed by Prism Central. + https://www.nutanix.com + + diff --git a/SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch b/SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch new file mode 100644 index 0000000..4915086 --- /dev/null +++ b/SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch @@ -0,0 +1,68 @@ +--- a/aws/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/aws/urllib3/response.py 2026-01-02 11:19:25.583808492 +0100 +@@ -135,8 +135,18 @@ + they were applied. + """ + ++ # Maximum allowed number of chained HTTP encodings in the ++ # Content-Encoding header. ++ max_decode_links = 5 ++ + def __init__(self, modes): +- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] ++ encodings = [m.strip() for m in modes.split(",")] ++ if len(encodings) > self.max_decode_links: ++ raise DecodeError( ++ "Too many content encodings in the chain: " ++ f"{len(encodings)} > {self.max_decode_links}" ++ ) ++ self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self): + return self._decoders[0].flush() + +--- a/azure/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/azure/urllib3/response.py 2026-01-02 11:19:25.583808492 +0100 +@@ -135,8 +135,18 @@ + they were applied. + """ + ++ # Maximum allowed number of chained HTTP encodings in the ++ # Content-Encoding header. ++ max_decode_links = 5 ++ + def __init__(self, modes): +- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] ++ encodings = [m.strip() for m in modes.split(",")] ++ if len(encodings) > self.max_decode_links: ++ raise DecodeError( ++ "Too many content encodings in the chain: " ++ f"{len(encodings)} > {self.max_decode_links}" ++ ) ++ self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self): + return self._decoders[0].flush() + +--- a/kubevirt/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/kubevirt/urllib3/response.py 2026-01-02 11:19:25.583808492 +0100 +@@ -135,8 +135,18 @@ + they were applied. + """ + ++ # Maximum allowed number of chained HTTP encodings in the ++ # Content-Encoding header. ++ max_decode_links = 5 ++ + def __init__(self, modes): +- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] ++ encodings = [m.strip() for m in modes.split(",")] ++ if len(encodings) > self.max_decode_links: ++ raise DecodeError( ++ "Too many content encodings in the chain: " ++ f"{len(encodings)} > {self.max_decode_links}" ++ ) ++ self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self): + return self._decoders[0].flush() diff --git a/SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch b/SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch new file mode 100644 index 0000000..3d7e28c --- /dev/null +++ b/SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch @@ -0,0 +1,845 @@ +--- a/aws/urllib3/response.py 2026-01-20 10:46:57.006470161 +0100 ++++ b/aws/urllib3/response.py 2026-01-20 10:55:44.090084896 +0100 +@@ -23,6 +23,7 @@ + from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, ++ DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, +@@ -41,34 +42,60 @@ + class DeflateDecoder(object): + def __init__(self): + self._first_try = True +- self._data = b"" ++ self._first_try_data = b"" ++ self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): +- if not data: ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ data = self._unfed_data + data ++ self._unfed_data = b"" ++ if not data and not self._obj.unconsumed_tail: + return data ++ original_max_length = max_length ++ if original_max_length < 0: ++ max_length = 0 ++ elif original_max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unfed_data = data ++ return b"" + ++ # Subsequent calls always reuse `self._obj`. zlib requires ++ # passing the unconsumed tail if decompression is to continue. + if not self._first_try: +- return self._obj.decompress(data) ++ return self._obj.decompress( ++ self._obj.unconsumed_tail + data, max_length=max_length ++ ) + +- self._data += data ++ # First call tries with RFC 1950 ZLIB format. ++ self._first_try_data += data + try: +- decompressed = self._obj.decompress(data) ++ decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False +- self._data = None ++ self._first_try_data = b"" + return decompressed ++ # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: +- return self.decompress(self._data) ++ return self.decompress( ++ self._first_try_data, max_length=original_max_length ++ ) + finally: +- self._data = None ++ self._first_try_data = b"" + ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unfed_data) or ( ++ bool(self._obj.unconsumed_tail) and not self._first_try ++ ) + + class GzipDecoderState(object): + +@@ -81,30 +108,64 @@ + def __init__(self): + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER ++ self._unconsumed_tail = b"" + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() +- if self._state == GzipDecoderState.SWALLOW_DATA or not data: ++ if self._state == GzipDecoderState.SWALLOW_DATA: ++ return bytes(ret) ++ ++ if max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unconsumed_tail += data ++ return b"" ++ ++ # zlib requires passing the unconsumed tail to the subsequent ++ # call if decompression is to continue. ++ data = self._unconsumed_tail + data ++ if not data and self._obj.eof: + return bytes(ret) ++ + while True: + try: +- ret += self._obj.decompress(data) ++ ret += self._obj.decompress( ++ data, max_length=max(max_length - len(ret), 0) ++ ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA ++ self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise +- data = self._obj.unused_data ++ ++ self._unconsumed_tail = data = ( ++ self._obj.unconsumed_tail or self._obj.unused_data ++ ) ++ if max_length > 0 and len(ret) >= max_length: ++ break ++ + if not data: + return bytes(ret) +- self._state = GzipDecoderState.OTHER_MEMBERS +- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ # When the end of a gzip member is reached, a new decompressor ++ # must be created for unused (possibly future) data. ++ if self._obj.eof: ++ self._state = GzipDecoderState.OTHER_MEMBERS ++ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ ++ return bytes(ret) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unconsumed_tail) + + + if brotli is not None: +@@ -116,9 +177,35 @@ + def __init__(self): + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): +- self.decompress = self._obj.decompress ++ setattr(self, "_decompress", self._obj.decompress) + else: +- self.decompress = self._obj.process ++ setattr(self, "_decompress", self._obj.process) ++ ++ # Requires Brotli >= 1.2.0 for `output_buffer_limit`. ++ def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: ++ raise NotImplementedError() ++ ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ try: ++ if max_length > 0: ++ return self._decompress(data, output_buffer_limit=max_length) ++ else: ++ return self._decompress(data) ++ except TypeError: ++ # Fallback for Brotli/brotlicffi/brotlipy versions without ++ # the `output_buffer_limit` parameter. ++ warnings.warn( ++ "Brotli >= 1.2.0 is required to prevent decompression bombs.", ++ DependencyWarning, ++ ) ++ return self._decompress(data) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ try: ++ return not self._obj.can_accept_more_data() ++ except AttributeError: ++ return False + + def flush(self): + if hasattr(self._obj, "flush"): +@@ -151,10 +238,35 @@ + def flush(self): + return self._decoders[0].flush() + +- def decompress(self, data): +- for d in reversed(self._decoders): +- data = d.decompress(data) +- return data ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ if max_length <= 0: ++ for d in reversed(self._decoders): ++ data = d.decompress(data) ++ return data ++ ++ ret = bytearray() ++ # Every while loop iteration goes through all decoders once. ++ # It exits when enough data is read or no more data can be read. ++ # It is possible that the while loop iteration does not produce ++ # any data because we retrieve up to `max_length` from every ++ # decoder, and the amount of bytes may be insufficient for the ++ # next decoder to produce enough/any output. ++ while True: ++ any_data = False ++ for d in reversed(self._decoders): ++ data = d.decompress(data, max_length=max_length - len(ret)) ++ if data: ++ any_data = True ++ # We should not break when no data is returned because ++ # next decoders may produce data even with empty input. ++ ret += data ++ if not any_data or len(ret) >= max_length: ++ return bytes(ret) ++ data = b"" ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return any(d.has_unconsumed_tail for d in self._decoders) + + + def _get_decoder(mode): +@@ -405,16 +517,25 @@ + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + +- def _decode(self, data, decode_content, flush_decoder): ++ def _decode( ++ self, ++ data: bytes, ++ decode_content: bool, ++ flush_decoder: bool, ++ max_length: int = None, ++ ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + return data + ++ if max_length is None or flush_decoder: ++ max_length = -1 ++ + try: + if self._decoder: +- data = self._decoder.decompress(data) ++ data = self._decoder.decompress(data, max_length=max_length) + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( +@@ -634,7 +755,10 @@ + for line in self.read_chunked(amt, decode_content=decode_content): + yield line + else: +- while not is_fp_closed(self._fp): ++ while ( ++ not is_fp_closed(self._fp) ++ or (self._decoder and self._decoder.has_unconsumed_tail) ++ ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: +@@ -840,7 +964,10 @@ + break + chunk = self._handle_chunk(amt) + decoded = self._decode( +- chunk, decode_content=decode_content, flush_decoder=False ++ chunk, ++ decode_content=decode_content, ++ flush_decoder=False, ++ max_length=amt, + ) + if decoded: + yield decoded + +--- a/azure/urllib3/response.py 2026-01-20 10:46:57.006470161 +0100 ++++ b/azure/urllib3/response.py 2026-01-20 10:55:44.090084896 +0100 +@@ -23,6 +23,7 @@ + from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, ++ DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, +@@ -41,34 +42,60 @@ + class DeflateDecoder(object): + def __init__(self): + self._first_try = True +- self._data = b"" ++ self._first_try_data = b"" ++ self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): +- if not data: ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ data = self._unfed_data + data ++ self._unfed_data = b"" ++ if not data and not self._obj.unconsumed_tail: + return data ++ original_max_length = max_length ++ if original_max_length < 0: ++ max_length = 0 ++ elif original_max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unfed_data = data ++ return b"" + ++ # Subsequent calls always reuse `self._obj`. zlib requires ++ # passing the unconsumed tail if decompression is to continue. + if not self._first_try: +- return self._obj.decompress(data) ++ return self._obj.decompress( ++ self._obj.unconsumed_tail + data, max_length=max_length ++ ) + +- self._data += data ++ # First call tries with RFC 1950 ZLIB format. ++ self._first_try_data += data + try: +- decompressed = self._obj.decompress(data) ++ decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False +- self._data = None ++ self._first_try_data = b"" + return decompressed ++ # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: +- return self.decompress(self._data) ++ return self.decompress( ++ self._first_try_data, max_length=original_max_length ++ ) + finally: +- self._data = None ++ self._first_try_data = b"" + ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unfed_data) or ( ++ bool(self._obj.unconsumed_tail) and not self._first_try ++ ) + + class GzipDecoderState(object): + +@@ -81,30 +108,64 @@ + def __init__(self): + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER ++ self._unconsumed_tail = b"" + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() +- if self._state == GzipDecoderState.SWALLOW_DATA or not data: ++ if self._state == GzipDecoderState.SWALLOW_DATA: ++ return bytes(ret) ++ ++ if max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unconsumed_tail += data ++ return b"" ++ ++ # zlib requires passing the unconsumed tail to the subsequent ++ # call if decompression is to continue. ++ data = self._unconsumed_tail + data ++ if not data and self._obj.eof: + return bytes(ret) ++ + while True: + try: +- ret += self._obj.decompress(data) ++ ret += self._obj.decompress( ++ data, max_length=max(max_length - len(ret), 0) ++ ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA ++ self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise +- data = self._obj.unused_data ++ ++ self._unconsumed_tail = data = ( ++ self._obj.unconsumed_tail or self._obj.unused_data ++ ) ++ if max_length > 0 and len(ret) >= max_length: ++ break ++ + if not data: + return bytes(ret) +- self._state = GzipDecoderState.OTHER_MEMBERS +- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ # When the end of a gzip member is reached, a new decompressor ++ # must be created for unused (possibly future) data. ++ if self._obj.eof: ++ self._state = GzipDecoderState.OTHER_MEMBERS ++ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ ++ return bytes(ret) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unconsumed_tail) + + + if brotli is not None: +@@ -116,9 +177,35 @@ + def __init__(self): + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): +- self.decompress = self._obj.decompress ++ setattr(self, "_decompress", self._obj.decompress) + else: +- self.decompress = self._obj.process ++ setattr(self, "_decompress", self._obj.process) ++ ++ # Requires Brotli >= 1.2.0 for `output_buffer_limit`. ++ def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: ++ raise NotImplementedError() ++ ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ try: ++ if max_length > 0: ++ return self._decompress(data, output_buffer_limit=max_length) ++ else: ++ return self._decompress(data) ++ except TypeError: ++ # Fallback for Brotli/brotlicffi/brotlipy versions without ++ # the `output_buffer_limit` parameter. ++ warnings.warn( ++ "Brotli >= 1.2.0 is required to prevent decompression bombs.", ++ DependencyWarning, ++ ) ++ return self._decompress(data) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ try: ++ return not self._obj.can_accept_more_data() ++ except AttributeError: ++ return False + + def flush(self): + if hasattr(self._obj, "flush"): +@@ -151,10 +238,35 @@ + def flush(self): + return self._decoders[0].flush() + +- def decompress(self, data): +- for d in reversed(self._decoders): +- data = d.decompress(data) +- return data ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ if max_length <= 0: ++ for d in reversed(self._decoders): ++ data = d.decompress(data) ++ return data ++ ++ ret = bytearray() ++ # Every while loop iteration goes through all decoders once. ++ # It exits when enough data is read or no more data can be read. ++ # It is possible that the while loop iteration does not produce ++ # any data because we retrieve up to `max_length` from every ++ # decoder, and the amount of bytes may be insufficient for the ++ # next decoder to produce enough/any output. ++ while True: ++ any_data = False ++ for d in reversed(self._decoders): ++ data = d.decompress(data, max_length=max_length - len(ret)) ++ if data: ++ any_data = True ++ # We should not break when no data is returned because ++ # next decoders may produce data even with empty input. ++ ret += data ++ if not any_data or len(ret) >= max_length: ++ return bytes(ret) ++ data = b"" ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return any(d.has_unconsumed_tail for d in self._decoders) + + + def _get_decoder(mode): +@@ -405,16 +517,25 @@ + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + +- def _decode(self, data, decode_content, flush_decoder): ++ def _decode( ++ self, ++ data: bytes, ++ decode_content: bool, ++ flush_decoder: bool, ++ max_length: int = None, ++ ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + return data + ++ if max_length is None or flush_decoder: ++ max_length = -1 ++ + try: + if self._decoder: +- data = self._decoder.decompress(data) ++ data = self._decoder.decompress(data, max_length=max_length) + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( +@@ -634,7 +755,10 @@ + for line in self.read_chunked(amt, decode_content=decode_content): + yield line + else: +- while not is_fp_closed(self._fp): ++ while ( ++ not is_fp_closed(self._fp) ++ or (self._decoder and self._decoder.has_unconsumed_tail) ++ ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: +@@ -840,7 +964,10 @@ + break + chunk = self._handle_chunk(amt) + decoded = self._decode( +- chunk, decode_content=decode_content, flush_decoder=False ++ chunk, ++ decode_content=decode_content, ++ flush_decoder=False, ++ max_length=amt, + ) + if decoded: + yield decoded + +--- a/kubevirt/urllib3/response.py 2026-01-20 10:46:57.006470161 +0100 ++++ b/kubevirt/urllib3/response.py 2026-01-20 10:55:44.090084896 +0100 +@@ -23,6 +23,7 @@ + from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, ++ DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, +@@ -41,34 +42,60 @@ + class DeflateDecoder(object): + def __init__(self): + self._first_try = True +- self._data = b"" ++ self._first_try_data = b"" ++ self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): +- if not data: ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ data = self._unfed_data + data ++ self._unfed_data = b"" ++ if not data and not self._obj.unconsumed_tail: + return data ++ original_max_length = max_length ++ if original_max_length < 0: ++ max_length = 0 ++ elif original_max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unfed_data = data ++ return b"" + ++ # Subsequent calls always reuse `self._obj`. zlib requires ++ # passing the unconsumed tail if decompression is to continue. + if not self._first_try: +- return self._obj.decompress(data) ++ return self._obj.decompress( ++ self._obj.unconsumed_tail + data, max_length=max_length ++ ) + +- self._data += data ++ # First call tries with RFC 1950 ZLIB format. ++ self._first_try_data += data + try: +- decompressed = self._obj.decompress(data) ++ decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False +- self._data = None ++ self._first_try_data = b"" + return decompressed ++ # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: +- return self.decompress(self._data) ++ return self.decompress( ++ self._first_try_data, max_length=original_max_length ++ ) + finally: +- self._data = None ++ self._first_try_data = b"" + ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unfed_data) or ( ++ bool(self._obj.unconsumed_tail) and not self._first_try ++ ) + + class GzipDecoderState(object): + +@@ -81,30 +108,64 @@ + def __init__(self): + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER ++ self._unconsumed_tail = b"" + + def __getattr__(self, name): + return getattr(self._obj, name) + +- def decompress(self, data): ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() +- if self._state == GzipDecoderState.SWALLOW_DATA or not data: ++ if self._state == GzipDecoderState.SWALLOW_DATA: ++ return bytes(ret) ++ ++ if max_length == 0: ++ # We should not pass 0 to the zlib decompressor because 0 is ++ # the default value that will make zlib decompress without a ++ # length limit. ++ # Data should be stored for subsequent calls. ++ self._unconsumed_tail += data ++ return b"" ++ ++ # zlib requires passing the unconsumed tail to the subsequent ++ # call if decompression is to continue. ++ data = self._unconsumed_tail + data ++ if not data and self._obj.eof: + return bytes(ret) ++ + while True: + try: +- ret += self._obj.decompress(data) ++ ret += self._obj.decompress( ++ data, max_length=max(max_length - len(ret), 0) ++ ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA ++ self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise +- data = self._obj.unused_data ++ ++ self._unconsumed_tail = data = ( ++ self._obj.unconsumed_tail or self._obj.unused_data ++ ) ++ if max_length > 0 and len(ret) >= max_length: ++ break ++ + if not data: + return bytes(ret) +- self._state = GzipDecoderState.OTHER_MEMBERS +- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ # When the end of a gzip member is reached, a new decompressor ++ # must be created for unused (possibly future) data. ++ if self._obj.eof: ++ self._state = GzipDecoderState.OTHER_MEMBERS ++ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) ++ ++ return bytes(ret) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return bool(self._unconsumed_tail) + + + if brotli is not None: +@@ -116,9 +177,35 @@ + def __init__(self): + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): +- self.decompress = self._obj.decompress ++ setattr(self, "_decompress", self._obj.decompress) + else: +- self.decompress = self._obj.process ++ setattr(self, "_decompress", self._obj.process) ++ ++ # Requires Brotli >= 1.2.0 for `output_buffer_limit`. ++ def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: ++ raise NotImplementedError() ++ ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ try: ++ if max_length > 0: ++ return self._decompress(data, output_buffer_limit=max_length) ++ else: ++ return self._decompress(data) ++ except TypeError: ++ # Fallback for Brotli/brotlicffi/brotlipy versions without ++ # the `output_buffer_limit` parameter. ++ warnings.warn( ++ "Brotli >= 1.2.0 is required to prevent decompression bombs.", ++ DependencyWarning, ++ ) ++ return self._decompress(data) ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ try: ++ return not self._obj.can_accept_more_data() ++ except AttributeError: ++ return False + + def flush(self): + if hasattr(self._obj, "flush"): +@@ -151,10 +238,35 @@ + def flush(self): + return self._decoders[0].flush() + +- def decompress(self, data): +- for d in reversed(self._decoders): +- data = d.decompress(data) +- return data ++ def decompress(self, data: bytes, max_length: int = -1) -> bytes: ++ if max_length <= 0: ++ for d in reversed(self._decoders): ++ data = d.decompress(data) ++ return data ++ ++ ret = bytearray() ++ # Every while loop iteration goes through all decoders once. ++ # It exits when enough data is read or no more data can be read. ++ # It is possible that the while loop iteration does not produce ++ # any data because we retrieve up to `max_length` from every ++ # decoder, and the amount of bytes may be insufficient for the ++ # next decoder to produce enough/any output. ++ while True: ++ any_data = False ++ for d in reversed(self._decoders): ++ data = d.decompress(data, max_length=max_length - len(ret)) ++ if data: ++ any_data = True ++ # We should not break when no data is returned because ++ # next decoders may produce data even with empty input. ++ ret += data ++ if not any_data or len(ret) >= max_length: ++ return bytes(ret) ++ data = b"" ++ ++ @property ++ def has_unconsumed_tail(self) -> bool: ++ return any(d.has_unconsumed_tail for d in self._decoders) + + + def _get_decoder(mode): +@@ -405,16 +517,25 @@ + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + +- def _decode(self, data, decode_content, flush_decoder): ++ def _decode( ++ self, ++ data: bytes, ++ decode_content: bool, ++ flush_decoder: bool, ++ max_length: int = None, ++ ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + return data + ++ if max_length is None or flush_decoder: ++ max_length = -1 ++ + try: + if self._decoder: +- data = self._decoder.decompress(data) ++ data = self._decoder.decompress(data, max_length=max_length) + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( +@@ -634,7 +755,10 @@ + for line in self.read_chunked(amt, decode_content=decode_content): + yield line + else: +- while not is_fp_closed(self._fp): ++ while ( ++ not is_fp_closed(self._fp) ++ or (self._decoder and self._decoder.has_unconsumed_tail) ++ ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: +@@ -840,7 +964,10 @@ + break + chunk = self._handle_chunk(amt) + decoded = self._decode( +- chunk, decode_content=decode_content, flush_decoder=False ++ chunk, ++ decode_content=decode_content, ++ flush_decoder=False, ++ max_length=amt, + ) + if decoded: + yield decoded diff --git a/SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch b/SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch new file mode 100644 index 0000000..f00b940 --- /dev/null +++ b/SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch @@ -0,0 +1,44 @@ +--- a/aws/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/aws/urllib3/response.py 2026-01-13 14:17:48.477104360 +0100 +@@ -292,7 +292,11 @@ + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: +- self.read() ++ self.read( ++ # Do not spend resources decoding the content unless ++ # decoding has already been initiated. ++ decode_content=self._has_decoded_content, ++ ) + except (HTTPError, SocketError, BaseSSLError, HTTPException): + pass + +--- a/azure/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/azure/urllib3/response.py 2026-01-13 14:17:48.477104360 +0100 +@@ -292,7 +292,11 @@ + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: +- self.read() ++ self.read( ++ # Do not spend resources decoding the content unless ++ # decoding has already been initiated. ++ decode_content=self._has_decoded_content, ++ ) + except (HTTPError, SocketError, BaseSSLError, HTTPException): + pass + +--- a/kubevirt/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200 ++++ b/kubevirt/urllib3/response.py 2026-01-13 14:17:48.477104360 +0100 +@@ -292,7 +292,11 @@ + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: +- self.read() ++ self.read( ++ # Do not spend resources decoding the content unless ++ # decoding has already been initiated. ++ decode_content=self._has_decoded_content, ++ ) + except (HTTPError, SocketError, BaseSSLError, HTTPException): + pass diff --git a/SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch b/SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch new file mode 100644 index 0000000..e7ba33d --- /dev/null +++ b/SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch @@ -0,0 +1,28 @@ +--- a/kubevirt/pyasn1/codec/ber/decoder.py 2019-10-17 07:00:19.000000000 +0200 ++++ b/kubevirt/pyasn1/codec/ber/decoder.py 2026-01-27 10:43:12.757563432 +0100 +@@ -22,6 +22,10 @@ + + noValue = base.noValue + ++# Maximum number of continuation octets (high-bit set) allowed per OID arc. ++# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs ++MAX_OID_ARC_CONTINUATION_OCTETS = 20 ++ + + class AbstractDecoder(object): + protoComponent = None +@@ -342,7 +346,14 @@ + # Construct subid from a number of octets + nextSubId = subId + subId = 0 ++ continuationOctetCount = 0 + while nextSubId >= 128: ++ continuationOctetCount += 1 ++ if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS: ++ raise error.PyAsn1Error( ++ 'OID arc exceeds maximum continuation octets limit (%d) ' ++ 'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index) ++ ) + subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( diff --git a/SPECS/fence-agents.spec b/SPECS/fence-agents.spec index 2a9eba2..6d61bee 100644 --- a/SPECS/fence-agents.spec +++ b/SPECS/fence-agents.spec @@ -87,7 +87,7 @@ Name: fence-agents Summary: Set of unified programs capable of host isolation ("fencing") Version: 4.2.1 -Release: 129%{?alphatag:.%{alphatag}}%{?dist}.15 +Release: 129%{?alphatag:.%{alphatag}}%{?dist}.21 License: GPLv2+ and LGPLv2+ Group: System Environment/Base URL: https://github.com/ClusterLabs/fence-agents @@ -320,6 +320,8 @@ Patch147: RHEL-99338-fence_aliyun-update.patch Patch148: RHEL-107506-fence_ibm_vpc-add-apikey-file-support.patch Patch149: RHEL-109814-1-fence_aws-add-skipshutdown-parameter.patch Patch150: RHEL-96179-fence_kubevirt-force-off.patch +Patch151: RHEL-110964-1-fence_nutanix_ahv.patch +Patch152: RHEL-110964-2-fence_nutanix_ahv-update-metadata.patch ### HA support libs/utils ### # all archs @@ -334,9 +336,13 @@ Patch2000: bz2218234-2-aws-fix-bundled-dateutil-CVE-2007-4559.patch Patch2001: RHEL-43568-2-aws-fix-bundled-urllib3-CVE-2024-37891.patch Patch2002: RHEL-104741-2-aliyun-aws-azure-fix-bundled-requests-CVE-2024-47081.patch Patch2003: RHEL-109814-2-botocore-add-SkipOsShutdown.patch +Patch2004: RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch +Patch2005: RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch +Patch2006: RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch +Patch2007: RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch %if 0%{?fedora} || 0%{?rhel} > 7 -%global supportedagents amt_ws apc apc_snmp bladecenter brocade cisco_mds cisco_ucs compute drac5 eaton_snmp emerson eps evacuate hds_cb hpblade ibmblade ibm_powervs ibm_vpc ifmib ilo ilo_moonshot ilo_mp ilo_ssh intelmodular ipdu ipmilan kdump kubevirt lpar mpath redfish rhevm rsa rsb sbd scsi vmware_rest vmware_soap wti +%global supportedagents amt_ws apc apc_snmp bladecenter brocade cisco_mds cisco_ucs compute drac5 eaton_snmp emerson eps evacuate hds_cb hpblade ibmblade ibm_powervs ibm_vpc ifmib ilo ilo_moonshot ilo_mp ilo_ssh intelmodular ipdu ipmilan kdump kubevirt lpar mpath nutanix_ahv redfish rhevm rsa rsb sbd scsi vmware_rest vmware_soap wti %ifarch x86_64 %global testagents virsh heuristics_ping aliyun aws azure_arm gce openstack %endif @@ -562,6 +568,8 @@ BuildRequires: python3-google-api-client python3-pip python3-wheel python3-jinja %patch -p1 -P 148 %patch -p1 -P 149 %patch -p1 -P 150 +%patch -p1 -P 151 +%patch -p1 -P 152 # prevent compilation of something that won't get used anyway sed -i.orig 's|FENCE_ZVM=1|FENCE_ZVM=0|' configure.ac @@ -691,6 +699,10 @@ pushd %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=2 < %{PATCH2001} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=2 < %{PATCH2002} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2003} +/usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2004} +/usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2005} +/usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2006} +/usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2007} %endif popd @@ -1410,6 +1422,19 @@ Device Mapper Multipath. %{_datadir}/cluster/fence_mpath_check* %{_mandir}/man8/fence_mpath.8* +%package nutanix-ahv +License: GPL-2.0-or-later AND LGPL-2.0-or-later +Summary: Fence agent for Nutanix AHV +Requires: python3-requests +Requires: fence-agents-common = %{version}-%{release} +BuildArch: noarch +Obsoletes: fence-agents < 3.1.13 +%description nutanix-ahv +Fence agent for Nutanix AHV clusters. +%files nutanix-ahv +%{_sbindir}/fence_nutanix_ahv +%{_mandir}/man8/fence_nutanix_ahv.8* + %ifarch x86_64 ppc64le %package openstack License: GPLv2+ and LGPLv2+ and ASL 2.0 and MIT and Python @@ -1611,6 +1636,23 @@ Fence agent for IBM z/VM over IP. %endif %changelog +* Tue Jan 27 2026 Oyvind Albrigtsen - 4.2.1-129.21 +- bundled pyasn1: fix CVE-2026-23490 + Resolves: RHEL-142447 + +* Mon Jan 19 2026 Oyvind Albrigtsen - 4.2.1-129.20 +- bundled urllib3: fix CVE-2025-66471 +- bundled urllib3: fix CVE-2026-21441 + Resolves: RHEL-139756, RHEL-140783 + +* Tue Jan 6 2026 Oyvind Albrigtsen - 4.2.1-129.17 +- bundled urllib3: fix CVE-2025-66418 + Resolves: RHEL-136027 + +* Mon Nov 3 2025 Oyvind Albrigtsen - 4.2.1-129.16 +- fence_nutanix_ahv: new fence agent + Resolves: RHEL-110964 + * Fri Sep 12 2025 Oyvind Albrigtsen - 4.2.1-129.15 - fence_kubevirt: use hard poweroff Resolves: RHEL-96179