diff --git a/README.md b/README.md index 4796ac0..64e7086 100644 --- a/README.md +++ b/README.md @@ -192,8 +192,23 @@ max_snapshots = 2 [vms.db1] node = "pve-node2" vmid = 200 + +# Example: Standalone Proxmox node with dedicated endpoint +# For non-clustered Proxmox nodes, specify per-node connection details +[vms.app1] +node = "bingus" +vmid = 300 +# Per-node endpoint for standalone (non-clustered) Proxmox nodes +endpoint = "https://bingus.example.com:8006" +username = "root@pam" # Optional: defaults to global config +password = "node-specific-password" # Optional: defaults to global config ``` +**Proxmox Cluster vs Standalone Nodes:** + +- **Clustered Nodes**: If your Proxmox nodes are in a cluster, you only need the global `endpoint` in `config.toml`. All nodes can be managed through a single API connection. +- **Standalone Nodes**: For independent Proxmox servers (not in a cluster), specify per-node `endpoint`, `username`, and `password` in the VM mapping. This allows managing VMs across multiple isolated Proxmox installations. + **Per-Host Snapshot Quota:** - Use `max_snapshots` to set a maximum number of automated snapshots per VM - When set, miniupdate will keep only the N newest snapshots and delete older ones diff --git a/miniupdate/update_automator.py b/miniupdate/update_automator.py index bd62d61..6605b62 100644 --- a/miniupdate/update_automator.py +++ b/miniupdate/update_automator.py @@ -66,20 +66,16 @@ def __init__(self, config: Config): self.ssh_config = config.ssh_config # Initialize components - self.proxmox_client = None + self.proxmox_clients = {} # Dictionary mapping endpoint -> ProxmoxClient + self.default_proxmox_config = None self.vm_mapper = None self.host_checker = HostChecker(self.ssh_config) # Setup Proxmox client if configured if self.proxmox_config: try: - self.proxmox_client = ProxmoxClient( - endpoint=self.proxmox_config['endpoint'], - username=self.proxmox_config['username'], - password=self.proxmox_config['password'], - verify_ssl=self.proxmox_config.get('verify_ssl', True), - timeout=self.proxmox_config.get('timeout', 30) - ) + # Store default Proxmox config for fallback + self.default_proxmox_config = self.proxmox_config # Setup VM mapper vm_mapping_file = self.proxmox_config.get('vm_mapping_file') @@ -87,8 +83,8 @@ def __init__(self, config: Config): logger.info("Proxmox integration enabled") except Exception as e: - logger.error(f"Failed to initialize Proxmox client: {e}") - self.proxmox_client = None + logger.error(f"Failed to initialize Proxmox configuration: {e}") + self.default_proxmox_config = None else: logger.info("Proxmox integration disabled - no configuration provided") @@ -120,6 +116,65 @@ def _resolve_vm_mapping_path(self, vm_mapping_file: Optional[str]) -> Optional[s return str(path_obj) + def _get_proxmox_client(self, vm_mapping: VMMapping) -> Optional[ProxmoxClient]: + """ + Get or create appropriate Proxmox client for the given VM mapping. + + Supports per-node endpoints for standalone (non-clustered) Proxmox nodes. + Falls back to global endpoint if no per-node endpoint is specified. + + Args: + vm_mapping: VM mapping containing optional per-node endpoint + + Returns: + ProxmoxClient instance or None if configuration is missing + """ + if not self.default_proxmox_config: + logger.error("No Proxmox configuration available") + return None + + # Determine endpoint, username, and password + # Priority: per-node config > global config + endpoint = vm_mapping.endpoint or self.default_proxmox_config.get('endpoint') + username = vm_mapping.username or self.default_proxmox_config.get('username') + password = vm_mapping.password or self.default_proxmox_config.get('password') + + if not endpoint or not username or not password: + logger.error(f"Incomplete Proxmox configuration for VM {vm_mapping.vmid} on node {vm_mapping.node}") + return None + + # Normalize endpoint for consistent key + endpoint = endpoint.rstrip('/') + + # Return existing client if already created for this endpoint + if endpoint in self.proxmox_clients: + return self.proxmox_clients[endpoint] + + # Create new client for this endpoint + try: + client = ProxmoxClient( + endpoint=endpoint, + username=username, + password=password, + verify_ssl=self.default_proxmox_config.get('verify_ssl', True), + timeout=self.default_proxmox_config.get('timeout', 30) + ) + + # Authenticate immediately to validate credentials + if not client.authenticate(): + logger.error(f"Failed to authenticate to Proxmox at {endpoint}") + return None + + # Cache the client + self.proxmox_clients[endpoint] = client + logger.info(f"Created Proxmox client for endpoint {endpoint}") + + return client + + except Exception as e: + logger.error(f"Failed to create Proxmox client for {endpoint}: {e}") + return None + def process_host_automated_update(self, host: Host, timeout: int = 120) -> AutomatedUpdateReport: """ Process automated updates for a single host. @@ -256,7 +311,7 @@ def process_host_automated_update(self, host: Host, timeout: int = 120) -> Autom f"({sum(1 for u in updates if u.security)} security)") # Create snapshot if Proxmox is configured and VM mapping exists - if self.proxmox_client and vm_mapping: + if self.default_proxmox_config and vm_mapping: snapshot_name = self._create_snapshot(vm_mapping, start_time) if not snapshot_name: return AutomatedUpdateReport( @@ -284,7 +339,7 @@ def process_host_automated_update(self, host: Host, timeout: int = 120) -> Autom command_output=error_output) # Revert snapshot if available - if snapshot_name and self.proxmox_client and vm_mapping: + if snapshot_name and self.default_proxmox_config and vm_mapping: if self._revert_snapshot(vm_mapping, snapshot_name): result = UpdateResult.REVERTED error_details += " - reverted to snapshot" @@ -319,7 +374,7 @@ def process_host_automated_update(self, host: Host, timeout: int = 120) -> Autom logger.info(f"Reboot after updates is disabled - skipping reboot for {host.name}") # Clean up snapshot if successful and configured - if (snapshot_name and self.proxmox_client and vm_mapping and + if (snapshot_name and self.default_proxmox_config and vm_mapping and self.update_config.get('cleanup_snapshots', False)): self._cleanup_old_snapshots(vm_mapping) @@ -354,11 +409,13 @@ def _create_snapshot(self, vm_mapping: VMMapping, start_time: datetime) -> Optio snapshot_name = f"{prefix}-{timestamp}" try: - if not self.proxmox_client.authenticate(): - logger.error("Failed to authenticate to Proxmox") + # Get appropriate Proxmox client for this VM + proxmox_client = self._get_proxmox_client(vm_mapping) + if not proxmox_client: + logger.error(f"Failed to get Proxmox client for VM {vm_mapping.vmid} on node {vm_mapping.node}") return None - response = self.proxmox_client.create_snapshot( + response = proxmox_client.create_snapshot( vm_mapping.node, vm_mapping.vmid, snapshot_name, @@ -369,7 +426,7 @@ def _create_snapshot(self, vm_mapping: VMMapping, start_time: datetime) -> Optio # Wait for snapshot task to complete if UPID is returned if 'data' in response and isinstance(response['data'], str): upid = response['data'] - if self.proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): + if proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): logger.info(f"Snapshot {snapshot_name} created successfully for VM {vm_mapping.vmid}") return snapshot_name else: @@ -387,9 +444,15 @@ def _create_snapshot(self, vm_mapping: VMMapping, start_time: datetime) -> Optio def _revert_snapshot(self, vm_mapping: VMMapping, snapshot_name: str) -> bool: """Revert VM to snapshot.""" try: + # Get appropriate Proxmox client for this VM + proxmox_client = self._get_proxmox_client(vm_mapping) + if not proxmox_client: + logger.error(f"Failed to get Proxmox client for VM {vm_mapping.vmid} on node {vm_mapping.node}") + return False + logger.warning(f"Reverting VM {vm_mapping.vmid} to snapshot {snapshot_name}") - response = self.proxmox_client.rollback_snapshot( + response = proxmox_client.rollback_snapshot( vm_mapping.node, vm_mapping.vmid, snapshot_name @@ -398,7 +461,7 @@ def _revert_snapshot(self, vm_mapping: VMMapping, snapshot_name: str) -> bool: # Wait for rollback task to complete if UPID is returned if 'data' in response and isinstance(response['data'], str): upid = response['data'] - if not self.proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): + if not proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): logger.error(f"Snapshot rollback task failed for VM {vm_mapping.vmid}") return False @@ -406,7 +469,7 @@ def _revert_snapshot(self, vm_mapping: VMMapping, snapshot_name: str) -> bool: # Ensure VM is powered on after rollback logger.info(f"Ensuring VM {vm_mapping.vmid} is powered on after snapshot restore") - if not self.proxmox_client.start_vm(vm_mapping.node, vm_mapping.vmid, timeout=60): + if not proxmox_client.start_vm(vm_mapping.node, vm_mapping.vmid, timeout=60): logger.error(f"Failed to power on VM {vm_mapping.vmid} after snapshot restore") return False @@ -429,7 +492,7 @@ def _handle_reboot_and_verification(self, host: Host, vm_mapping: Optional[VMMap error_details = "Failed to send reboot command" # Revert snapshot if available - if snapshot_name and self.proxmox_client and vm_mapping: + if snapshot_name and self.default_proxmox_config and vm_mapping: if self._revert_snapshot(vm_mapping, snapshot_name): result = UpdateResult.REVERTED error_details += " - reverted to snapshot" @@ -461,7 +524,7 @@ def _handle_reboot_and_verification(self, host: Host, vm_mapping: Optional[VMMap error_details = f"Host did not become available within {ping_timeout} seconds after reboot" # Revert snapshot if available - if snapshot_name and self.proxmox_client and vm_mapping: + if snapshot_name and self.default_proxmox_config and vm_mapping: if self._revert_snapshot(vm_mapping, snapshot_name): result = UpdateResult.REVERTED error_details += " - reverted to snapshot" @@ -488,9 +551,15 @@ def _handle_reboot_and_verification(self, host: Host, vm_mapping: Optional[VMMap def _cleanup_old_snapshots(self, vm_mapping: VMMapping): """Clean up old automated snapshots based on retention policy or count limit.""" try: + # Get appropriate Proxmox client for this VM + proxmox_client = self._get_proxmox_client(vm_mapping) + if not proxmox_client: + logger.error(f"Failed to get Proxmox client for VM {vm_mapping.vmid} on node {vm_mapping.node}") + return + prefix = self.update_config.get('snapshot_name_prefix', 'pre-update') - snapshots = self.proxmox_client.list_snapshots(vm_mapping.node, vm_mapping.vmid) + snapshots = proxmox_client.list_snapshots(vm_mapping.node, vm_mapping.vmid) # Filter to only automated snapshots with valid timestamps automated_snapshots = [] @@ -549,7 +618,7 @@ def _cleanup_old_snapshots(self, vm_mapping: VMMapping): for snap in snapshots_to_delete: snap_name = snap['name'] logger.info(f"Deleting old snapshot {snap_name} for VM {vm_mapping.vmid}") - self.proxmox_client.delete_snapshot(vm_mapping.node, vm_mapping.vmid, snap_name) + proxmox_client.delete_snapshot(vm_mapping.node, vm_mapping.vmid, snap_name) except Exception as e: logger.warning(f"Failed to cleanup old snapshots for VM {vm_mapping.vmid}: {e}") \ No newline at end of file diff --git a/miniupdate/vm_mapping.py b/miniupdate/vm_mapping.py index 4a77942..ba85623 100644 --- a/miniupdate/vm_mapping.py +++ b/miniupdate/vm_mapping.py @@ -19,6 +19,9 @@ class VMMapping(NamedTuple): vmid: int host_name: str max_snapshots: Optional[int] = None + endpoint: Optional[str] = None # Optional per-node Proxmox endpoint + username: Optional[str] = None # Optional per-node credentials + password: Optional[str] = None # Optional per-node credentials class VMMapper: @@ -74,6 +77,9 @@ def _load_mappings(self) -> Dict[str, VMMapping]: node = vm_info.get('node') vmid = vm_info.get('vmid') max_snapshots = vm_info.get('max_snapshots') + endpoint = vm_info.get('endpoint') # Optional per-node endpoint + username = vm_info.get('username') # Optional per-node credentials + password = vm_info.get('password') # Optional per-node credentials if not node or not vmid: logger.warning(f"Incomplete VM mapping for {host_name}: " @@ -101,7 +107,10 @@ def _load_mappings(self) -> Dict[str, VMMapping]: node=node, vmid=vmid, host_name=host_name, - max_snapshots=max_snapshots + max_snapshots=max_snapshots, + endpoint=endpoint, + username=username, + password=password ) logger.info(f"Loaded VM mappings for {len(mappings)} hosts") @@ -146,6 +155,14 @@ def create_example_vm_mapping(path: str = "vm_mapping.toml.example") -> None: "db1": { "node": "pve-node2", "vmid": 200 + }, + "app1": { + "node": "bingus", + "vmid": 300, + # Optional: Per-node Proxmox endpoint for standalone (non-clustered) nodes + "endpoint": "https://bingus.example.com:8006", + "username": "root@pam", # Optional: defaults to global config + "password": "node-specific-password" # Optional: defaults to global config } } } @@ -154,6 +171,10 @@ def create_example_vm_mapping(path: str = "vm_mapping.toml.example") -> None: # Write with comments f.write("# VM Mapping Configuration for miniupdate\n") f.write("# Maps Ansible inventory host names to Proxmox VM IDs and nodes\n") - f.write("# Optional: Set max_snapshots per VM to limit snapshot count for capacity-limited storage\n\n") + f.write("# Optional: Set max_snapshots per VM to limit snapshot count for capacity-limited storage\n") + f.write("#\n") + f.write("# For Proxmox clusters: Only the global endpoint in config.toml is needed\n") + f.write("# For standalone nodes: Specify per-node 'endpoint', 'username', and 'password'\n") + f.write("# (username and password default to global config if not specified)\n\n") toml.dump(example_config, f) diff --git a/proxmox-per-node-endpoint.patch b/proxmox-per-node-endpoint.patch new file mode 100644 index 0000000..8750fe5 --- /dev/null +++ b/proxmox-per-node-endpoint.patch @@ -0,0 +1,316 @@ +diff --git a/README.md b/README.md +index 4796ac0..64e7086 100644 +--- a/README.md ++++ b/README.md +@@ -192,8 +192,23 @@ max_snapshots = 2 + [vms.db1] + node = "pve-node2" + vmid = 200 ++ ++# Example: Standalone Proxmox node with dedicated endpoint ++# For non-clustered Proxmox nodes, specify per-node connection details ++[vms.app1] ++node = "bingus" ++vmid = 300 ++# Per-node endpoint for standalone (non-clustered) Proxmox nodes ++endpoint = "https://bingus.example.com:8006" ++username = "root@pam" # Optional: defaults to global config ++password = "node-specific-password" # Optional: defaults to global config + ``` + ++**Proxmox Cluster vs Standalone Nodes:** ++ ++- **Clustered Nodes**: If your Proxmox nodes are in a cluster, you only need the global `endpoint` in `config.toml`. All nodes can be managed through a single API connection. ++- **Standalone Nodes**: For independent Proxmox servers (not in a cluster), specify per-node `endpoint`, `username`, and `password` in the VM mapping. This allows managing VMs across multiple isolated Proxmox installations. ++ + **Per-Host Snapshot Quota:** + - Use `max_snapshots` to set a maximum number of automated snapshots per VM + - When set, miniupdate will keep only the N newest snapshots and delete older ones +diff --git a/miniupdate/update_automator.py b/miniupdate/update_automator.py +index 2661d4f..ef36dc0 100644 +--- a/miniupdate/update_automator.py ++++ b/miniupdate/update_automator.py +@@ -68,20 +68,16 @@ class UpdateAutomator: + self.ssh_config = config.ssh_config + + # Initialize components +- self.proxmox_client = None ++ self.proxmox_clients = {} # Dictionary mapping endpoint -> ProxmoxClient ++ self.default_proxmox_config = None + self.vm_mapper = None + self.host_checker = HostChecker(self.ssh_config) + + # Setup Proxmox client if configured + if self.proxmox_config: + try: +- self.proxmox_client = ProxmoxClient( +- endpoint=self.proxmox_config["endpoint"], +- username=self.proxmox_config["username"], +- password=self.proxmox_config["password"], +- verify_ssl=self.proxmox_config.get("verify_ssl", True), +- timeout=self.proxmox_config.get("timeout", 30), +- ) ++ # Store default Proxmox config for fallback ++ self.default_proxmox_config = self.proxmox_config + + # Setup VM mapper + vm_mapping_file = self.proxmox_config.get("vm_mapping_file") +@@ -91,8 +87,8 @@ class UpdateAutomator: + + logger.info("Proxmox integration enabled") + except Exception as e: +- logger.error(f"Failed to initialize Proxmox client: {e}") +- self.proxmox_client = None ++ logger.error(f"Failed to initialize Proxmox configuration: {e}") ++ self.default_proxmox_config = None + else: + logger.info("Proxmox integration disabled - no configuration provided") + +@@ -124,6 +120,68 @@ class UpdateAutomator: + + return str(path_obj) + ++ def _get_proxmox_client(self, vm_mapping: VMMapping) -> Optional[ProxmoxClient]: ++ """ ++ Get or create appropriate Proxmox client for the given VM mapping. ++ ++ Supports per-node endpoints for standalone (non-clustered) Proxmox nodes. ++ Falls back to global endpoint if no per-node endpoint is specified. ++ ++ Args: ++ vm_mapping: VM mapping containing optional per-node endpoint ++ ++ Returns: ++ ProxmoxClient instance or None if configuration is missing ++ """ ++ if not self.default_proxmox_config: ++ logger.error("No Proxmox configuration available") ++ return None ++ ++ # Determine endpoint, username, and password ++ # Priority: per-node config > global config ++ endpoint = vm_mapping.endpoint or self.default_proxmox_config.get("endpoint") ++ username = vm_mapping.username or self.default_proxmox_config.get("username") ++ password = vm_mapping.password or self.default_proxmox_config.get("password") ++ ++ if not endpoint or not username or not password: ++ logger.error( ++ f"Incomplete Proxmox configuration for VM {vm_mapping.vmid} " ++ f"on node {vm_mapping.node}" ++ ) ++ return None ++ ++ # Normalize endpoint for consistent key ++ endpoint = endpoint.rstrip("/") ++ ++ # Return existing client if already created for this endpoint ++ if endpoint in self.proxmox_clients: ++ return self.proxmox_clients[endpoint] ++ ++ # Create new client for this endpoint ++ try: ++ client = ProxmoxClient( ++ endpoint=endpoint, ++ username=username, ++ password=password, ++ verify_ssl=self.default_proxmox_config.get("verify_ssl", True), ++ timeout=self.default_proxmox_config.get("timeout", 30), ++ ) ++ ++ # Authenticate immediately to validate credentials ++ if not client.authenticate(): ++ logger.error(f"Failed to authenticate to Proxmox at {endpoint}") ++ return None ++ ++ # Cache the client ++ self.proxmox_clients[endpoint] = client ++ logger.info(f"Created Proxmox client for endpoint {endpoint}") ++ ++ return client ++ ++ except Exception as e: ++ logger.error(f"Failed to create Proxmox client for {endpoint}: {e}") ++ return None ++ + def process_host_automated_update( + self, host: Host, timeout: int = 120 + ) -> AutomatedUpdateReport: +@@ -287,7 +345,7 @@ class UpdateAutomator: + ) + + # Create snapshot if Proxmox is configured and VM mapping exists +- if self.proxmox_client and vm_mapping: ++ if self.default_proxmox_config and vm_mapping: + snapshot_name = self._create_snapshot(vm_mapping, start_time) + if not snapshot_name: + return AutomatedUpdateReport( +@@ -406,11 +464,16 @@ class UpdateAutomator: + snapshot_name = f"{prefix}-{timestamp}" + + try: +- if not self.proxmox_client.authenticate(): +- logger.error("Failed to authenticate to Proxmox") ++ # Get appropriate Proxmox client for this VM ++ proxmox_client = self._get_proxmox_client(vm_mapping) ++ if not proxmox_client: ++ logger.error( ++ f"Failed to get Proxmox client for VM {vm_mapping.vmid} " ++ f"on node {vm_mapping.node}" ++ ) + return None + +- response = self.proxmox_client.create_snapshot( ++ response = proxmox_client.create_snapshot( + vm_mapping.node, + vm_mapping.vmid, + snapshot_name, +@@ -421,9 +484,7 @@ class UpdateAutomator: + # Wait for snapshot task to complete if UPID is returned + if "data" in response and isinstance(response["data"], str): + upid = response["data"] +- if self.proxmox_client.wait_for_task( +- vm_mapping.node, upid, timeout=300 +- ): ++ if proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): + logger.info( + f"Snapshot {snapshot_name} created successfully for VM {vm_mapping.vmid}" + ) +@@ -447,20 +508,27 @@ class UpdateAutomator: + def _revert_snapshot(self, vm_mapping: VMMapping, snapshot_name: str) -> bool: + """Revert VM to snapshot.""" + try: ++ # Get appropriate Proxmox client for this VM ++ proxmox_client = self._get_proxmox_client(vm_mapping) ++ if not proxmox_client: ++ logger.error( ++ f"Failed to get Proxmox client for VM {vm_mapping.vmid} " ++ f"on node {vm_mapping.node}" ++ ) ++ return False ++ + logger.warning( + f"Reverting VM {vm_mapping.vmid} to snapshot {snapshot_name}" + ) + +- response = self.proxmox_client.rollback_snapshot( ++ response = proxmox_client.rollback_snapshot( + vm_mapping.node, vm_mapping.vmid, snapshot_name + ) + + # Wait for rollback task to complete if UPID is returned + if "data" in response and isinstance(response["data"], str): + upid = response["data"] +- if not self.proxmox_client.wait_for_task( +- vm_mapping.node, upid, timeout=300 +- ): ++ if not proxmox_client.wait_for_task(vm_mapping.node, upid, timeout=300): + logger.error( + f"Snapshot rollback task failed for VM {vm_mapping.vmid}" + ) +@@ -472,7 +540,7 @@ class UpdateAutomator: + logger.info( + f"Ensuring VM {vm_mapping.vmid} is powered on after snapshot restore" + ) +- if not self.proxmox_client.start_vm( ++ if not proxmox_client.start_vm( + vm_mapping.node, vm_mapping.vmid, timeout=60 + ): + logger.error( +@@ -567,11 +635,18 @@ class UpdateAutomator: + def _cleanup_old_snapshots(self, vm_mapping: VMMapping): + """Clean up old automated snapshots based on retention policy or count limit.""" + try: ++ # Get appropriate Proxmox client for this VM ++ proxmox_client = self._get_proxmox_client(vm_mapping) ++ if not proxmox_client: ++ logger.error( ++ f"Failed to get Proxmox client for VM {vm_mapping.vmid} " ++ f"on node {vm_mapping.node}" ++ ) ++ return ++ + prefix = self.update_config.get("snapshot_name_prefix", "pre-update") + +- snapshots = self.proxmox_client.list_snapshots( +- vm_mapping.node, vm_mapping.vmid +- ) ++ snapshots = proxmox_client.list_snapshots(vm_mapping.node, vm_mapping.vmid) + + # Filter to only automated snapshots with valid timestamps + automated_snapshots = [] +@@ -644,7 +719,7 @@ class UpdateAutomator: + logger.info( + f"Deleting old snapshot {snap_name} for VM {vm_mapping.vmid}" + ) +- self.proxmox_client.delete_snapshot( ++ proxmox_client.delete_snapshot( + vm_mapping.node, vm_mapping.vmid, snap_name + ) + +diff --git a/miniupdate/vm_mapping.py b/miniupdate/vm_mapping.py +index a0abc8a..03f63f4 100644 +--- a/miniupdate/vm_mapping.py ++++ b/miniupdate/vm_mapping.py +@@ -22,6 +22,9 @@ class VMMapping(NamedTuple): + vmid: int + host_name: str + max_snapshots: Optional[int] = None ++ endpoint: Optional[str] = None # Optional per-node Proxmox endpoint ++ username: Optional[str] = None # Optional per-node credentials ++ password: Optional[str] = None # Optional per-node credentials + + + class VMMapper: +@@ -79,6 +82,9 @@ class VMMapper: + node = vm_info.get("node") + vmid = vm_info.get("vmid") + max_snapshots = vm_info.get("max_snapshots") ++ endpoint = vm_info.get("endpoint") # Optional per-node endpoint ++ username = vm_info.get("username") # Optional per-node credentials ++ password = vm_info.get("password") # Optional per-node credentials + + if not node or not vmid: + logger.warning( +@@ -115,6 +121,9 @@ class VMMapper: + vmid=vmid, + host_name=host_name, + max_snapshots=max_snapshots, ++ endpoint=endpoint, ++ username=username, ++ password=password, + ) + + logger.info("Loaded VM mappings for %s hosts", len(mappings)) +diff --git a/vm_mapping.toml.example b/vm_mapping.toml.example +index 15ba29d..2fa5466 100644 +--- a/vm_mapping.toml.example ++++ b/vm_mapping.toml.example +@@ -1,5 +1,10 @@ + # VM Mapping Configuration for miniupdate + # Maps Ansible inventory host names to Proxmox VM IDs and nodes ++# Optional: Set max_snapshots per VM to limit snapshot count for capacity-limited storage ++# ++# For Proxmox clusters: Only the global endpoint in config.toml is needed ++# For standalone nodes: Specify per-node 'endpoint', 'username', and 'password' ++# (username and password default to global config if not specified) + + [vms.web1] + node = "pve-node1" +@@ -8,7 +13,17 @@ vmid = 100 + [vms.web2] + node = "pve-node1" + vmid = 101 ++max_snapshots = 2 # Optional: limit to 2 snapshots for capacity-limited storage + + [vms.db1] + node = "pve-node2" + vmid = 200 ++ ++# Example: Standalone Proxmox node with dedicated endpoint ++[vms.app1] ++node = "bingus" ++vmid = 300 ++# Optional: Per-node Proxmox endpoint for standalone (non-clustered) nodes ++endpoint = "https://bingus.example.com:8006" ++username = "root@pam" # Optional: defaults to global config ++password = "node-specific-password" # Optional: defaults to global config diff --git a/vm_mapping.toml.example b/vm_mapping.toml.example index 15ba29d..2fa5466 100644 --- a/vm_mapping.toml.example +++ b/vm_mapping.toml.example @@ -1,5 +1,10 @@ # VM Mapping Configuration for miniupdate # Maps Ansible inventory host names to Proxmox VM IDs and nodes +# Optional: Set max_snapshots per VM to limit snapshot count for capacity-limited storage +# +# For Proxmox clusters: Only the global endpoint in config.toml is needed +# For standalone nodes: Specify per-node 'endpoint', 'username', and 'password' +# (username and password default to global config if not specified) [vms.web1] node = "pve-node1" @@ -8,7 +13,17 @@ vmid = 100 [vms.web2] node = "pve-node1" vmid = 101 +max_snapshots = 2 # Optional: limit to 2 snapshots for capacity-limited storage [vms.db1] node = "pve-node2" vmid = 200 + +# Example: Standalone Proxmox node with dedicated endpoint +[vms.app1] +node = "bingus" +vmid = 300 +# Optional: Per-node Proxmox endpoint for standalone (non-clustered) nodes +endpoint = "https://bingus.example.com:8006" +username = "root@pam" # Optional: defaults to global config +password = "node-specific-password" # Optional: defaults to global config