Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 180 additions & 18 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,75 @@ def proxmox_api_call(connection, api_func, *args, **kwargs):
return None


def update_cluster_next_id(proxmox, vmid):
"""Update the cluster's next-id lower bound after VM/container creation.

This prevents VMID reuse when incremental_vmid is enabled for the cluster.
It sets the next-id lower bound to vmid + 1, but only if this would
increase the lower bound (not decrease it).

Args:
proxmox: Proxmox API connection
vmid: The VMID that was just created
"""
try:
# Check if incremental_vmid is enabled for current cluster
if current_cluster_id and current_cluster_id in all_clusters:
cluster_config = all_clusters[current_cluster_id]
if not cluster_config.get("incremental_vmid", False):
return # Feature not enabled

# Get current cluster options to preserve existing bounds
try:
options = proxmox.cluster.options.get()
current_next_id = options.get("next-id", "")
except Exception:
current_next_id = ""

# Parse existing lower and upper bounds
current_lower = None
upper_bound = None
if current_next_id:
# Format: "lower=X,upper=Y" or just values
for part in current_next_id.split(","):
part = part.strip()
if part.startswith("lower="):
try:
current_lower = int(part.split("=")[1])
except (ValueError, IndexError):
pass
elif part.startswith("upper="):
try:
upper_bound = int(part.split("=")[1])
except (ValueError, IndexError):
pass

# Calculate new lower bound
new_lower = int(vmid) + 1

# Only update if new lower bound is greater than current
if current_lower is not None and new_lower <= current_lower:
print(
f"Skipping next-id update: new lower {new_lower} <= "
f"current lower {current_lower}"
)
return

# Build new next-id value
if upper_bound:
next_id_value = f"lower={new_lower},upper={upper_bound}"
else:
next_id_value = f"lower={new_lower}"

# Update cluster options
proxmox.cluster.options.put(**{"next-id": next_id_value})
print(f"Updated cluster next-id lower bound to {new_lower}")

except Exception as e:
# Don't fail VM creation if next-id update fails
print(f"Warning: Failed to update cluster next-id: {e}")


def get_proxmox_for_node(node_name):
"""Get the appropriate Proxmox connection for a specific node"""
# First check if we have a direct connection to this node
Expand Down Expand Up @@ -1090,6 +1159,7 @@ def connect():
token_name = request.form.get("token_name")
token_value = request.form.get("token_value")
verify_ssl = "verify_ssl" in request.form
incremental_vmid = "incremental_vmid" in request.form

# Check if cluster ID already exists
if cluster_id in all_clusters:
Expand Down Expand Up @@ -1135,6 +1205,7 @@ def connect():
new_cluster = {
"id": cluster_id,
"name": cluster_name,
"incremental_vmid": incremental_vmid,
"nodes": [node_config],
}

Expand Down Expand Up @@ -1307,6 +1378,7 @@ def api_get_cluster(cluster_id):
"cluster": {
"id": cluster_id,
"name": cluster_config.get("name", cluster_id),
"incremental_vmid": cluster_config.get("incremental_vmid", False),
"nodes": nodes,
},
}
Expand Down Expand Up @@ -1336,6 +1408,10 @@ def api_update_cluster(cluster_id):
if "name" in data:
config["clusters"][i]["name"] = data["name"]

# Update incremental_vmid setting if provided
if "incremental_vmid" in data:
config["clusters"][i]["incremental_vmid"] = data["incremental_vmid"]

# Update nodes if provided
if "nodes" in data and len(data["nodes"]) > 0:
node_data = data["nodes"][0] # Currently support single node
Expand Down Expand Up @@ -1875,6 +1951,8 @@ def create_vm():
proxmox.nodes(node).qemu(template_vmid).clone.post(
newid=vmid, name=name, full=1 # Full clone
)
# Update next-id if incremental VMID is enabled
update_cluster_next_id(proxmox, vmid)
flash(f"VM {name} cloned from template successfully", "success")

elif vm_type == "qemu":
Expand Down Expand Up @@ -1905,6 +1983,8 @@ def create_vm():

# Create the VM
proxmox.nodes(node).qemu.create(**params)
# Update next-id if incremental VMID is enabled
update_cluster_next_id(proxmox, vmid)

elif vm_type == "lxc":
# Create container
Expand All @@ -1931,6 +2011,8 @@ def create_vm():

# Create the container
proxmox.nodes(node).lxc.create(**params)
# Update next-id if incremental VMID is enabled
update_cluster_next_id(proxmox, vmid)

flash(f"{vm_type.upper()} {name} created successfully", "success")
return redirect(url_for("vms"))
Expand Down Expand Up @@ -2948,6 +3030,9 @@ def api_create_lxc(node):
# Create the container
task = proxmox.nodes(node).lxc.post(**params)

# Update next-id if incremental VMID is enabled
update_cluster_next_id(proxmox, vmid)

return jsonify(
{
"success": True,
Expand Down Expand Up @@ -3004,6 +3089,49 @@ def api_next_vmid():
return jsonify({"error": str(e), "vmid": 100}), 500


@app.route("/api/cluster/vmid/<vmid>/check")
def api_check_vmid(vmid):
"""API endpoint to check if a VMID is available (not in use)"""
if not proxmox_nodes:
return jsonify({"error": "No Proxmox connections available"}), 500

try:
# Validate VMID is a number
vmid_int = int(vmid)
if vmid_int < 100:
return jsonify({"available": False, "reason": "VMID must be >= 100"})

# Get any working connection with auto-renewal
node_name = next(iter(proxmox_nodes.keys()))
proxmox = get_proxmox_connection(node_name, auto_renew=True)

if not proxmox:
return jsonify({"error": "No valid Proxmox connection available"}), 500

# Get all VMs and containers from cluster resources
resources = proxmox.cluster.resources.get(type="vm")

# Check if VMID is in use
for resource in resources:
if resource.get("vmid") == vmid_int:
return jsonify(
{
"available": False,
"reason": f"VMID {vmid} is already in use",
"name": resource.get("name", ""),
"node": resource.get("node", ""),
"type": resource.get("type", ""),
}
)

return jsonify({"available": True})

except ValueError:
return jsonify({"available": False, "reason": "Invalid VMID format"})
except Exception as e:
return jsonify({"error": str(e)}), 500


@app.route("/api/vm/<node>/<vmid>/tasks")
def api_vm_tasks(node, vmid):
"""API endpoint to get recent tasks for a specific VM/container"""
Expand Down Expand Up @@ -3683,6 +3811,9 @@ def api_vm_clone(node, vmid):
# Execute clone operation
result = proxmox.nodes(node).qemu(vmid).clone.post(**clone_params)

# Update next-id if incremental VMID is enabled
update_cluster_next_id(proxmox, clone_vmid)

return jsonify(
{
"success": True,
Expand All @@ -3699,40 +3830,68 @@ def api_vm_clone(node, vmid):

@app.route("/api/vm/<node>/<vmid>/delete", methods=["POST"])
def api_vm_delete(node, vmid):
"""API endpoint to delete a VM"""
"""API endpoint to delete a VM or container"""
proxmox = get_proxmox_connection(node, auto_renew=True)
if not proxmox:
return jsonify({"error": "Node not found"}), 404

try:
# Get VM status first
vm_status = proxmox.nodes(node).qemu(vmid).status.current.get()
# Detect if this is a QEMU VM or LXC container
vm_type = None
vm_status = None
vm_name = vmid

# Try QEMU first
try:
vm_status = proxmox.nodes(node).qemu(vmid).status.current.get()
vm_type = "qemu"
try:
vm_config = proxmox.nodes(node).qemu(vmid).config.get()
vm_name = vm_config.get("name", vmid)
except Exception:
pass
except Exception:
pass

# Try LXC if QEMU failed
if vm_type is None:
try:
vm_status = proxmox.nodes(node).lxc(vmid).status.current.get()
vm_type = "lxc"
try:
vm_config = proxmox.nodes(node).lxc(vmid).config.get()
vm_name = vm_config.get("hostname", vmid)
except Exception:
pass
except Exception:
pass

# Check if VM is stopped
if vm_type is None:
return jsonify({"error": f"VM/Container {vmid} does not exist"}), 404

# Check if VM/container is stopped
if vm_status.get("status") != "stopped":
return (
jsonify(
{
"error": f"VM must be stopped before deletion. Current status: {vm_status.get('status')}"
"error": f"{'Container' if vm_type == 'lxc' else 'VM'} must be stopped before deletion. Current status: {vm_status.get('status')}"
}
),
400,
)

# Get VM config for name
try:
vm_config = proxmox.nodes(node).qemu(vmid).config.get()
vm_name = vm_config.get("name", vmid)
except:
vm_name = vmid

# Delete the VM (this will remove disks by default)
result = proxmox.nodes(node).qemu(vmid).delete(purge=1)
# Delete the VM or container
if vm_type == "lxc":
result = proxmox.nodes(node).lxc(vmid).delete(purge=1)
type_label = "Container"
else:
result = proxmox.nodes(node).qemu(vmid).delete(purge=1)
type_label = "VM"

return jsonify(
{
"success": True,
"message": f"VM {vm_name} (ID: {vmid}) has been deleted successfully",
"message": f"{type_label} {vm_name} (ID: {vmid}) has been deleted successfully",
"task": result,
}
)
Expand All @@ -3742,11 +3901,14 @@ def api_vm_delete(node, vmid):

# Check for common error patterns
if "does not exist" in error_msg.lower():
return jsonify({"error": f"VM {vmid} does not exist"}), 404
return jsonify({"error": f"VM/Container {vmid} does not exist"}), 404
elif "not stopped" in error_msg.lower() or "running" in error_msg.lower():
return jsonify({"error": "VM must be stopped before deletion"}), 400
return (
jsonify({"error": "VM/Container must be stopped before deletion"}),
400,
)
else:
return jsonify({"error": f"Failed to delete VM: {error_msg}"}), 500
return jsonify({"error": f"Failed to delete: {error_msg}"}), 500


# =============================================================================
Expand Down
3 changes: 3 additions & 0 deletions config.toml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
[[clusters]]
id = "main"
name = "My Proxmox Cluster 1"
# When enabled, prevents VMID reuse by updating the cluster's next-id lower bound
# after each VM/container creation. This avoids mixing backups and monitoring data.
incremental_vmid = false

[[clusters.nodes]]
host = "x.y.z.1"
Expand Down
20 changes: 19 additions & 1 deletion templates/connect.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,18 @@ <h5 class="text-primary mb-3">

<div class="mb-3">
<label for="cluster_id" class="form-label">Cluster ID</label>
<input type="text" class="form-control" id="cluster_id" name="cluster_id"
<input type="text" class="form-control" id="cluster_id" name="cluster_id"
value="main" required pattern="[a-zA-Z0-9_-]+">
<div class="form-text">Unique identifier (letters, numbers, underscores, and dashes only)</div>
</div>

<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="incremental_vmid" name="incremental_vmid">
<label class="form-check-label" for="incremental_vmid">
Incremental VMID (prevent reuse)
</label>
<div class="form-text">When enabled, updates the cluster's next-id lower bound after each VM/container creation to prevent VMID reuse. This avoids mixing backups and monitoring data from deleted VMs.</div>
</div>
</div>

<!-- Node Connection -->
Expand Down Expand Up @@ -347,6 +355,7 @@ <h6>API Token Setup:</h6>
document.getElementById('edit_node_host').value = node.host || '';
document.getElementById('edit_node_user').value = node.user || '';
document.getElementById('edit_verify_ssl').checked = node.verify_ssl !== false;
document.getElementById('edit_incremental_vmid').checked = cluster.incremental_vmid || false;

// Set auth type
if (node.auth_type === 'token') {
Expand Down Expand Up @@ -448,6 +457,7 @@ <h6>API Token Setup:</h6>

const data = {
name: document.getElementById('edit_cluster_name').value,
incremental_vmid: document.getElementById('edit_incremental_vmid').checked,
nodes: [{
host: document.getElementById('edit_node_host').value,
user: document.getElementById('edit_node_user').value,
Expand Down Expand Up @@ -520,6 +530,14 @@ <h6 class="text-primary mb-3">
<label for="edit_cluster_name" class="form-label">Cluster Name</label>
<input type="text" class="form-control" id="edit_cluster_name" required>
</div>

<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="edit_incremental_vmid">
<label class="form-check-label" for="edit_incremental_vmid">
Incremental VMID (prevent reuse)
</label>
<div class="form-text">When enabled, updates the cluster's next-id lower bound after each VM/container creation to prevent VMID reuse.</div>
</div>
</div>

<!-- Node Connection -->
Expand Down
Loading