Import from CS git
This commit is contained in:
parent
9929d5f165
commit
453069547c
790
SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch
Normal file
790
SOURCES/RHEL-110964-1-fence_nutanix_ahv.patch
Normal file
@ -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 @@
|
||||
+<?xml version="1.0" ?>
|
||||
+<resource-agent name="fence_nutanix_ahv" shortdesc="Fencing agent for Nutanix AHV Cluster VMs." >
|
||||
+<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.</longdesc>
|
||||
+<vendor-url>https://www.nutanix.com</vendor-url>
|
||||
+<parameters>
|
||||
+ <parameter name="action" unique="0" required="1">
|
||||
+ <getopt mixed="-o, --action=[action]" />
|
||||
+ <content type="string" default="reboot" />
|
||||
+ <shortdesc lang="en">Fencing action</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ip" unique="0" required="1" obsoletes="ipaddr">
|
||||
+ <getopt mixed="-a, --ip=[ip]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">IP address or hostname of fencing device</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ipaddr" unique="0" required="1" deprecated="1">
|
||||
+ <getopt mixed="-a, --ip=[ip]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">IP address or hostname of fencing device</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ipport" unique="0" required="0">
|
||||
+ <getopt mixed="-u, --ipport=[port]" />
|
||||
+ <content type="integer" default="80" />
|
||||
+ <shortdesc lang="en">TCP/UDP port to use for connection with device</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="login" unique="0" required="1" deprecated="1">
|
||||
+ <getopt mixed="-l, --username=[name]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Login name</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="method" unique="0" required="0">
|
||||
+ <getopt mixed="-m, --method=[method]" />
|
||||
+ <content type="select" default="onoff" >
|
||||
+ <option value="onoff" />
|
||||
+ <option value="cycle" />
|
||||
+ </content>
|
||||
+ <shortdesc lang="en">Method to fence</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="notls" unique="0" required="0">
|
||||
+ <getopt mixed="-t, --notls" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Disable TLS negotiation and force SSL3.0. This should only be used for devices that do not support TLS1.0 and up.</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="passwd" unique="0" required="0" deprecated="1">
|
||||
+ <getopt mixed="-p, --password=[password]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Login password or passphrase</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="passwd_script" unique="0" required="0" deprecated="1">
|
||||
+ <getopt mixed="-S, --password-script=[script]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Script to run to retrieve password</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="password" unique="0" required="0" obsoletes="passwd">
|
||||
+ <getopt mixed="-p, --password=[password]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Login password or passphrase</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="password_script" unique="0" required="0" obsoletes="passwd_script">
|
||||
+ <getopt mixed="-S, --password-script=[script]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Script to run to retrieve password</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="plug" unique="0" required="1" obsoletes="port">
|
||||
+ <getopt mixed="-n, --plug=[id]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="port" unique="0" required="1" deprecated="1">
|
||||
+ <getopt mixed="-n, --plug=[id]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ssl" unique="0" required="0">
|
||||
+ <getopt mixed="-z, --ssl" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Use SSL connection with verifying certificate</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ssl_insecure" unique="0" required="0">
|
||||
+ <getopt mixed="--ssl-insecure" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Use SSL connection without verifying certificate</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="ssl_secure" unique="0" required="0">
|
||||
+ <getopt mixed="--ssl-secure" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Use SSL connection with verifying certificate</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="username" unique="0" required="1" obsoletes="login">
|
||||
+ <getopt mixed="-l, --username=[name]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Login name</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="filter" unique="0" required="0">
|
||||
+ <getopt mixed="
|
||||
+ --filter=[filter]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Filter list, get_liste.g: "name eq 'node1-vm'"</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="quiet" unique="0" required="0">
|
||||
+ <getopt mixed="-q, --quiet" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="verbose" unique="0" required="0">
|
||||
+ <getopt mixed="-v, --verbose" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity.</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="verbose_level" unique="0" required="0">
|
||||
+ <getopt mixed="--verbose-level" />
|
||||
+ <content type="integer" />
|
||||
+ <shortdesc lang="en">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).</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="debug" unique="0" required="0" deprecated="1">
|
||||
+ <getopt mixed="-D, --debug-file=[debugfile]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Write debug information to given file</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="debug_file" unique="0" required="0" obsoletes="debug">
|
||||
+ <getopt mixed="-D, --debug-file=[debugfile]" />
|
||||
+ <shortdesc lang="en">Write debug information to given file</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="version" unique="0" required="0">
|
||||
+ <getopt mixed="-V, --version" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Display version information and exit</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="help" unique="0" required="0">
|
||||
+ <getopt mixed="-h, --help" />
|
||||
+ <content type="boolean" />
|
||||
+ <shortdesc lang="en">Display help and exit</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="plug_separator" unique="0" required="0">
|
||||
+ <getopt mixed="--plug-separator=[char]" />
|
||||
+ <content type="string" default="," />
|
||||
+ <shortdesc lang="en">Separator for plug parameter when specifying more than 1 plug</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="separator" unique="0" required="0">
|
||||
+ <getopt mixed="-C, --separator=[char]" />
|
||||
+ <content type="string" default="," />
|
||||
+ <shortdesc lang="en">Separator for CSV created by 'list' operation</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="delay" unique="0" required="0">
|
||||
+ <getopt mixed="--delay=[seconds]" />
|
||||
+ <content type="second" default="0" />
|
||||
+ <shortdesc lang="en">Wait X seconds before fencing is started</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="disable_timeout" unique="0" required="0">
|
||||
+ <getopt mixed="--disable-timeout=[true/false]" />
|
||||
+ <content type="string" />
|
||||
+ <shortdesc lang="en">Disable timeout (true/false) (default: true when run from Pacemaker 2.0+)</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="login_timeout" unique="0" required="0">
|
||||
+ <getopt mixed="--login-timeout=[seconds]" />
|
||||
+ <content type="second" default="5" />
|
||||
+ <shortdesc lang="en">Wait X seconds for cmd prompt after login</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="power_timeout" unique="0" required="0">
|
||||
+ <getopt mixed="--power-timeout=[seconds]" />
|
||||
+ <content type="second" default="60" />
|
||||
+ <shortdesc lang="en">Test X seconds for status change after ON/OFF</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="power_wait" unique="0" required="0">
|
||||
+ <getopt mixed="--power-wait=[seconds]" />
|
||||
+ <content type="second" default="0" />
|
||||
+ <shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="shell_timeout" unique="0" required="0">
|
||||
+ <getopt mixed="--shell-timeout=[seconds]" />
|
||||
+ <content type="second" default="3" />
|
||||
+ <shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="stonith_status_sleep" unique="0" required="0">
|
||||
+ <getopt mixed="--stonith-status-sleep=[seconds]" />
|
||||
+ <content type="second" default="1" />
|
||||
+ <shortdesc lang="en">Sleep X seconds between status calls during a STONITH action</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="retry_on" unique="0" required="0">
|
||||
+ <getopt mixed="--retry-on=[attempts]" />
|
||||
+ <content type="integer" default="1" />
|
||||
+ <shortdesc lang="en">Count of attempts to retry power on</shortdesc>
|
||||
+ </parameter>
|
||||
+ <parameter name="gnutlscli_path" unique="0" required="0">
|
||||
+ <getopt mixed="--gnutlscli-path=[path]" />
|
||||
+ <shortdesc lang="en">Path to gnutls-cli binary</shortdesc>
|
||||
+ </parameter>
|
||||
+</parameters>
|
||||
+<actions>
|
||||
+ <action name="on" automatic="0"/>
|
||||
+ <action name="off" />
|
||||
+ <action name="reboot" />
|
||||
+ <action name="status" />
|
||||
+ <action name="list" />
|
||||
+ <action name="list-status" />
|
||||
+ <action name="monitor" />
|
||||
+ <action name="metadata" />
|
||||
+ <action name="manpage" />
|
||||
+ <action name="validate-all" />
|
||||
+</actions>
|
||||
+</resource-agent>
|
||||
@ -0,0 +1,39 @@
|
||||
From 345a609a34ea153e168b15d48eb8f2dd8d017f04 Mon Sep 17 00:00:00 2001
|
||||
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
|
||||
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 @@
|
||||
<?xml version="1.0" ?>
|
||||
-<resource-agent name="fence_nutanix_ahv" shortdesc="Fencing agent for Nutanix AHV Cluster VMs." >
|
||||
-<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.</longdesc>
|
||||
+<resource-agent name="fence_nutanix_ahv" shortdesc="Fence agent for Nutanix AHV Cluster VMs." >
|
||||
+<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.</longdesc>
|
||||
<vendor-url>https://www.nutanix.com</vendor-url>
|
||||
<parameters>
|
||||
<parameter name="action" unique="0" required="1">
|
||||
68
SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch
Normal file
68
SOURCES/RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch
Normal file
@ -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()
|
||||
845
SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch
Normal file
845
SOURCES/RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch
Normal file
@ -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
|
||||
44
SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch
Normal file
44
SOURCES/RHEL-140783-fix-bundled-urllib3-CVE-2026-21441.patch
Normal file
@ -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
|
||||
28
SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch
Normal file
28
SOURCES/RHEL-142447-fix-bundled-pyasn1-CVE-2026-23490.patch
Normal file
@ -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(
|
||||
@ -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 <oalbrigt@redhat.com> - 4.2.1-129.21
|
||||
- bundled pyasn1: fix CVE-2026-23490
|
||||
Resolves: RHEL-142447
|
||||
|
||||
* Mon Jan 19 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 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 <oalbrigt@redhat.com> - 4.2.1-129.17
|
||||
- bundled urllib3: fix CVE-2025-66418
|
||||
Resolves: RHEL-136027
|
||||
|
||||
* Mon Nov 3 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.2.1-129.16
|
||||
- fence_nutanix_ahv: new fence agent
|
||||
Resolves: RHEL-110964
|
||||
|
||||
* Fri Sep 12 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.2.1-129.15
|
||||
- fence_kubevirt: use hard poweroff
|
||||
Resolves: RHEL-96179
|
||||
|
||||
Loading…
Reference in New Issue
Block a user