|
| 1 | +""" |
| 2 | +The Patching class is responsible for orchestrating the patching process of nodes in a Proxmox cluster, |
| 3 | +based on the provided ProxLB data and using the Proxmox API. It determines which nodes require |
| 4 | +patching, selects nodes for patching according to configuration, and executes patching actions |
| 5 | +while ensuring no running guests are present. |
| 6 | +""" |
| 7 | + |
| 8 | + |
| 9 | +__author__ = "Florian Paul Azim Hoberg <gyptazy>" |
| 10 | +__copyright__ = "Copyright (C) 2025 Florian Paul Azim Hoberg (@gyptazy)" |
| 11 | +__license__ = "GPL-3.0" |
| 12 | + |
| 13 | + |
| 14 | +from utils.logger import SystemdLogger |
| 15 | +from typing import Dict, Any |
| 16 | + |
| 17 | +logger = SystemdLogger() |
| 18 | + |
| 19 | + |
| 20 | +class Patching: |
| 21 | + """ |
| 22 | + Patching |
| 23 | +
|
| 24 | + This class is responsible for orchestrating the patching process of nodes in a Proxmox cluster, |
| 25 | + based on the provided ProxLB data and using the Proxmox API. It determines which nodes require |
| 26 | + patching, selects nodes for patching according to configuration, and executes patching actions |
| 27 | + while ensuring no running guests are present. |
| 28 | +
|
| 29 | + Functions: |
| 30 | + ----------- |
| 31 | + __init__(self, proxmox_api: any, proxlb_data: Dict[str, Any], calculations_done: bool = False) |
| 32 | + - Initializes the Patching class and triggers either patch preparation or execution based on the calculations_done flag. |
| 33 | + - Inputs: |
| 34 | + - proxmox_api: Proxmox API client instance. |
| 35 | + - proxlb_data: Dictionary containing cluster and node information. |
| 36 | + - calculations_done: Boolean flag to determine operation mode. |
| 37 | + - Outputs: None |
| 38 | +
|
| 39 | + val_nodes_packages(self, proxmox_api: any, proxlb_data: Dict[str, Any]) -> Dict[str, Any] |
| 40 | + - Checks each node for available package updates and updates their patching status. |
| 41 | + - Inputs: |
| 42 | + - proxmox_api: Proxmox API client instance. |
| 43 | + - proxlb_data: Dictionary with node and maintenance information. |
| 44 | + - Outputs: |
| 45 | + - Updated proxlb_data dictionary with patching status for each node. |
| 46 | +
|
| 47 | + get_nodes_to_patch(self, proxlb_data: Dict[str, Any]) -> Dict[str, Any] |
| 48 | + - Selects nodes to patch in the current run based on configuration and node status. |
| 49 | + - Inputs: |
| 50 | + - proxlb_data: Dictionary with ProxLB configuration and node information. |
| 51 | + - Outputs: |
| 52 | + - Updated proxlb_data with selected nodes for patching in this run. |
| 53 | +
|
| 54 | + patch_node(self, proxmox_api: any, proxlb_data: Dict[str, Any]) |
| 55 | + - Executes the patching process for selected nodes, ensuring no running guests are present before proceeding. |
| 56 | + - Inputs: |
| 57 | + - proxmox_api: Proxmox API client instance. |
| 58 | + - proxlb_data: Dictionary with metadata and list of nodes to patch. |
| 59 | + - Outputs: None |
| 60 | + """ |
| 61 | + def __init__(self, proxmox_api: any, proxlb_data: Dict[str, Any], calculations_done: bool = False): |
| 62 | + """ |
| 63 | + Initializes the Patching class with the provided ProxLB data. |
| 64 | + """ |
| 65 | + if not calculations_done: |
| 66 | + logger.debug("Starting: Patching preparations.") |
| 67 | + self.val_nodes_packages(proxmox_api, proxlb_data) |
| 68 | + self.get_nodes_to_patch(proxlb_data) |
| 69 | + logger.debug("Finished: Patching preparations.") |
| 70 | + else: |
| 71 | + logger.debug("Starting: Patching executions.") |
| 72 | + self.patch_node(proxmox_api, proxlb_data) |
| 73 | + logger.debug("Finished: Patching executions.") |
| 74 | + |
| 75 | + def val_nodes_packages(self, proxmox_api: any, proxlb_data: Dict[str, Any]) -> Dict[str, Any]: |
| 76 | + """ |
| 77 | + Checks each node in the provided ProxLB data for available package updates using the Proxmox API, |
| 78 | + and updates the node's patching status accordingly. |
| 79 | +
|
| 80 | + Args: |
| 81 | + proxmox_api (Any): An instance of the Proxmox API client used to query node package updates. |
| 82 | + proxlb_data (Dict[str, Any]): A dictionary containing node information, including maintenance status. |
| 83 | +
|
| 84 | + Returns: |
| 85 | + Dict[str, Any]: The updated proxlb_data dictionary with patching status set for each node. |
| 86 | + """ |
| 87 | + logger.debug("Starting: val_nodes_packages.") |
| 88 | + |
| 89 | + for node in proxlb_data['nodes'].keys(): |
| 90 | + if proxlb_data['nodes'][node]['maintenance'] is False: |
| 91 | + node_pkgs = proxmox_api.nodes(node).apt.update.get() |
| 92 | + |
| 93 | + if len(node_pkgs) > 0: |
| 94 | + proxlb_data['nodes'][node]['patching'] = True |
| 95 | + logger.debug(f"Node {node} has {len(node_pkgs)} packages to update.") |
| 96 | + else: |
| 97 | + logger.debug(f"Node {node} is up to date and has no packages to update.") |
| 98 | + |
| 99 | + logger.debug("Finished: val_nodes_packages.") |
| 100 | + return proxlb_data |
| 101 | + |
| 102 | + def get_nodes_to_patch(self, proxlb_data: Dict[str, Any]): |
| 103 | + """ |
| 104 | + Determines which nodes should be patched in the current run based on the ProxLB configuration and node status. |
| 105 | +
|
| 106 | + Args: |
| 107 | + proxlb_data (Dict[str, Any]): A dictionary containing ProxLB configuration, metadata, and node information. |
| 108 | + - proxlb_data["meta"]["patching"]["maximum_nodes"]: Maximum number of nodes to patch in this run (default is 1). |
| 109 | + - proxlb_data["nodes"]: Dictionary of node objects, each with a "patching" status and "name". |
| 110 | +
|
| 111 | + Returns: |
| 112 | + Dict[str, Any]: The updated proxlb_data dictionary with: |
| 113 | + - proxlb_data["meta"]["patching"]: List of node names selected for patching in this run. |
| 114 | + - proxlb_data["nodes"]: Updated node objects with "patching" status set to True for selected nodes. |
| 115 | + """ |
| 116 | + logger.debug("Starting: get_node_patching.") |
| 117 | + |
| 118 | + nodes_patching_execution = [] |
| 119 | + nodes_patching_count = proxlb_data["meta"].get("patching", {}).get("maximum_nodes", 1) |
| 120 | + nodes_patching = [node for node in proxlb_data["nodes"].values() if node["patching"]] |
| 121 | + nodes_patching_sorted = sorted(nodes_patching, key=lambda x: x["name"]) |
| 122 | + logger.debug(f"{len(nodes_patching)} nodes are pending for patching. Patching up to {nodes_patching_count} nodes in this run.") |
| 123 | + |
| 124 | + if len(nodes_patching_sorted) > 0: |
| 125 | + nodes = nodes_patching_sorted[:nodes_patching_count] |
| 126 | + for node in nodes: |
| 127 | + nodes_patching_execution.append(node["name"]) |
| 128 | + proxlb_data['nodes'][node['name']]['patching'] = True |
| 129 | + logger.info(f"Node {node['name']} is going to be patched.") |
| 130 | + logger.info(f"Node {node['name']} is set to maintenance.") |
| 131 | + |
| 132 | + proxlb_data["meta"]["patching"] = nodes_patching_execution |
| 133 | + |
| 134 | + logger.debug("Finished: get_node_patching.") |
| 135 | + return proxlb_data |
| 136 | + |
| 137 | + def patch_node(self, proxmox_api: any, proxlb_data:Dict[str, Any]): |
| 138 | + """ |
| 139 | + Patches Proxmox nodes if no running guests are detected. |
| 140 | +
|
| 141 | + This method iterates over the nodes specified in the `proxlb_data` dictionary under the "meta" -> "patching" key. |
| 142 | + For each node, it checks for running QEMU (VM) and LXC (container) guests using the provided Proxmox API client. |
| 143 | + If any guests are running, patching is skipped for that node and a warning is logged. |
| 144 | + If no guests are running, the method proceeds to patch the node (API calls are commented out) and logs the actions. |
| 145 | + Rebooting the node after patching is also logged (API call commented out). |
| 146 | +
|
| 147 | + Args: |
| 148 | + proxmox_api (Any): An instance of the Proxmox API client used to interact with the cluster. |
| 149 | + proxlb_data (Dict[str, Any]): A dictionary containing metadata, including the list of nodes to patch under "meta" -> "patching". |
| 150 | +
|
| 151 | + Returns: |
| 152 | + None |
| 153 | + """ |
| 154 | + logger.debug("Starting: patch_node.") |
| 155 | + |
| 156 | + for node in proxlb_data["meta"]["patching"]: |
| 157 | + node_guests = [] |
| 158 | + guests_vm = proxmox_api.nodes(node).qemu.get() |
| 159 | + guests_ct = proxmox_api.nodes(node).lxc.get() |
| 160 | + guests_vm = [vm for vm in guests_vm if vm["status"] == "running"] |
| 161 | + guests_ct = [ct for ct in guests_ct if ct["status"] == "running"] |
| 162 | + guests_count = len(guests_vm) + len(guests_ct) |
| 163 | + |
| 164 | + # Do not proceed when we still have someho guests running on the node |
| 165 | + if guests_vm or guests_ct: |
| 166 | + logger.warning(f"Node {node} has {guests_count} running guest(s). Patching will be skipped.") |
| 167 | + else: |
| 168 | + logger.debug(f"Node {node} has no running guests. Proceeding with patching.") |
| 169 | + # Upgrading a node by API requires the patched 'pve-manager' package |
| 170 | + # from gyptazy including the new 'upgrade' endpoint. |
| 171 | + #proxmox_api.nodes(node).apt.upgrade.post() |
| 172 | + logger.debug(f"Node {node} has been patched.") |
| 173 | + logger.debug(f"Node {node} is going to reboot.") |
| 174 | + #proxmox_api.nodes(node).status.reboot.post() |
| 175 | + |
| 176 | + logger.debug("Finished: patch_node.") |
0 commit comments