Skip to content

Commit f8af107

Browse files
doc changes
1 parent 7a41d67 commit f8af107

File tree

6 files changed

+172
-21
lines changed

6 files changed

+172
-21
lines changed

README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,10 @@ If you see errors like this...
3939
4040
Try uninstalling `mkdocs` from your package manager, (e.g. `brew uninstall mkdocs`) and just using the version installed by `pip`. It seems that `mkdocs` doesn't like it when you've installed it using different methods.
4141

42-
# What's New in 1.2.0
43-
- Adds LXC support
44-
- Adds the ability to define 'vmid' for both VM and LXC, rather than taking a default value from Proxmox
45-
- Adds "discovery" of Proxmox VM and LXC disks and auto-creation of VM disk objects in NetBox
46-
- Adds rudimentary Proxmox VM and LXC discovery through convenience script
47-
- Adds AWX initial setup through convenience script (uses awxkit)
48-
- Convenience script changes to accommodate LXC requirements
49-
- Can dynamically build webhooks and event rules from current AWX state through convenience script
50-
- Adds customization changes for LXC-specific requirements
42+
# What's New in 2025.11.01
43+
- Adds Proxmox cluster and node(s) discovery through a new convenience script
44+
- Adds Proxmox VM migration to alternate Proxmox node(s) through events
45+
- Adds [NetBox Branching](https://netboxlabs.com/docs/extensions/branching/) support for Proxmox node and VM discovery
5146

5247
# Developers
5348
- Nate Patwardhan <[email protected]>
@@ -56,9 +51,10 @@ If you see errors like this...
5651

5752
## Known Issues
5853
- *Only* supports SCSI disk types (this is possibly fine as Proxmox predomininantly provisions disks as SCSI)
59-
- Does not currently support Proxmox VM creation to a Proxmox cluster, but is only node-based
54+
- LXC migration is not supported for myriad reasons
55+
- Proxmox "tags" are not supported (seeking community feedback around use cases)
6056

6157
## Roadmap -- Delivery
58+
- Use of NetBox Custom Objects for NetBox > 4.4
59+
- DNS update support via gss-tsig (requires NetBox `netbox-dns` plugin)
6260
- Integration with NetBox Discovery/Assurance
63-
- DNS update support (requires NetBox `netbox-dns` plugin)
64-
- Maybe evolve into to a NetBox plugin for Proxmox

docs/configure-awx-aap.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ When you create *any* template in AWX for Proxmox automation, you will need to s
251251
| awx-proxmox-remove-vm-disk.yml | Removes a non-OS disk (i.e. not scsi0) from a Proxmox VM |
252252
| awx-proxmox-remove-vm.yml | Removes a Proxmox VM |
253253
| awx-proxmox-resize-vm-disk.yml | Resizes a Proxmox VM disk |
254-
| awx-proxmox-set-ipconfig0.yml | Sets ipconfig0 for Proxmox VM and adds ssh key|
254+
| awx-proxmox-set-ipconfig0.yml | Sets ipconfig0 for Proxmox VM and adds ssh key |
255255
| awx-proxmox-start-vm.yml | Starts Proxmox VM |
256256
| awx-proxmox-stop-vm.yml | Stops Proxmox VM |
257+
| awx-proxmox-migrate-vm.yml | Migrates Proxmox VM to an alternate Proxmox node |
257258

docs/netbox-event-rules-and-webhooks-flask.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Regardless of whether you are using a Flask (or other) application for Proxmox a
2222

2323
### Flask Application
2424

25-
As noted [here](#initial-configuration-flask-application-python), you will need to have a running Flask application *before* you can start handling events (i.e. object changes) inside of NetBox.
25+
You will need to have a running Flask application *before* you can start handling events (i.e. object changes) inside of NetBox.
2626

2727
#### Automated Webhook and Event Rules Configuration
2828

docs/usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ For LXC automation, `netbox-proxmox-automation` uses LXC (container images) on P
1717

1818
NetBox models VMs in an intuitive way. You can define roles for VMs, such as for Proxmox, and from there you can define both VM state (Staged, Active, Offline, etc) and other resources like vcpus, memory, network configuration, VM disks, and more (through customizations in NetBox).
1919

20-
This automation is based on the premise(s) that:
20+
This automation *requires* that:
2121

2222
1. You are using Python (version 3)
23-
2. You are using NetBox 4.1.0 or newer (NetBox 3.7.x should also work)
24-
3. You have a running Proxmox instance or cluster
23+
2. You are using NetBox 4.3.7 or newer
24+
3. You are running Proxmox 8.4.x single node or cluster (Proxmox 9.x is *not* tested)
2525
4. You have a running [AWX](https://github.com/ansible/awx) instance or are running [your own web application](https://github.com/netboxlabs/netbox-proxmox-automation/tree/main/netbox-event-driven-automation-flask-app) to handle webhooks and event rules
2626
5. You have converted a cloud-init image to a Proxmox VM template
2727
6. Your Promox VM template(s) has/have qemu-guest-agent installed, and that qemu-guest-agent has been enabled via cloud-init
@@ -31,7 +31,7 @@ This automation is based on the premise(s) that:
3131

3232
## What this implementation *is not*
3333

34-
`netbox-proxmox-automation` is not currently a NetBox plugin, but this may change.
34+
`netbox-proxmox-automation` is not a NetBox plugin, and future aspirations of this implementation will employ [custom objects](https://github.com/netboxlabs/netbox-custom-objects).
3535

3636
[ProxBox](https://github.com/netdevopsbr/netbox-proxbox) is a neat implementation of pulling information from Proxmox into NetBox. ProxBox has its place, most certainly, but what it does is *not* the aim of `netbox-proxmox-automation`.
3737

netbox-event-driven-automation-flask-app/app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
# adapted from: https://majornetwork.net/2019/10/webhook-listener-for-netbox/
88

9-
from helpers.netbox_proxmox import NetBoxProxmoxHelper, NetBoxProxmoxHelperVM, NetBoxProxmoxHelperLXC
9+
from helpers.netbox_proxmox import NetBoxProxmoxHelper, NetBoxProxmoxHelperVM, NetBoxProxmoxHelperLXC, NetBoxProxmoxHelperMigrate
1010

1111
from flask import Flask, Response, request, jsonify
1212
from flask_restx import Api, Resource, fields
1313

14-
VERSION = '1.2.0'
14+
VERSION = '2025.11.01'
1515

1616
app_config_file = 'app_config.yml'
1717

@@ -128,8 +128,14 @@ def post(self):
128128
results = tc.proxmox_delete_vm(webhook_json_data)
129129
elif webhook_json_data['event'] == 'updated':
130130
if webhook_json_data['data']['status']['value'] == 'offline':
131+
132+
# if source node != target node -> migrate
133+
131134
results = tc.proxmox_stop_vm(webhook_json_data)
132135
elif webhook_json_data['data']['status']['value'] == 'active':
136+
137+
# if source node != target node -> migrate
138+
133139
results = tc.proxmox_start_vm(webhook_json_data)
134140
else:
135141
results = (500, {'result': f"Unknown value {webhook_json_data['data']['status']['value']}"})

netbox-event-driven-automation-flask-app/helpers/netbox_proxmox.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import re
21
import pynetbox
2+
import re
33
import requests
4+
import time
45
import urllib
56

67
from proxmoxer import ProxmoxAPI, ResourceException
@@ -591,3 +592,150 @@ def proxmox_delete_lxc(self, json_in):
591592
return 500, {'result': e.content}
592593

593594

595+
class NetBoxProxmoxHelperMigrate(NetBoxProxmoxHelper):
596+
def __init__(self):
597+
self.proxmox_cluster_name = 'default-proxmox-cluster-name'
598+
self.proxmox_nodes = {}
599+
self.proxmox_vms = {}
600+
self.proxmox_lxc = {}
601+
602+
self.__get_cluster_name_and_nodes()
603+
self.__get_proxmox_vms()
604+
self.__get_proxmox_lxcs()
605+
606+
607+
def __get_cluster_name_and_nodes(self):
608+
try:
609+
cluster_status = self.proxmox_api.cluster.status.get()
610+
611+
for resource in cluster_status:
612+
if not 'type' in resource:
613+
raise ValueError(f"Missing 'type' in Proxmox cluster resource {resource}")
614+
615+
if resource['type'] == 'cluster':
616+
self.proxmox_cluster_name = resource['name']
617+
elif resource['type'] == 'node':
618+
if not resource['name'] in self.proxmox_nodes:
619+
self.proxmox_nodes[resource['name']] = {}
620+
621+
self.proxmox_nodes[resource['name']]['ip'] = resource['ip']
622+
self.proxmox_nodes[resource['name']]['online'] = resource['online']
623+
except ResourceException as e:
624+
raise RuntimeError(f"Proxmox API error: {e}") from e
625+
except requests.exceptions.ConnectionError:
626+
raise RuntimeError("Failed to connect to Proxmox API")
627+
except requests.exceptions.HTTPError as e:
628+
status = e.response.status_code
629+
raise RuntimeError(f"HTTP {status}: {e.response.text}") from e
630+
631+
632+
def __get_proxmox_vms(self):
633+
try:
634+
for proxmox_node in self.proxmox_nodes:
635+
all_vm_settings = self.proxmox_api.nodes(proxmox_node).get('qemu')
636+
for vm_setting in all_vm_settings:
637+
if 'template' in vm_setting and vm_setting['template'] == 1:
638+
continue
639+
640+
if not vm_setting['name'] in self.proxmox_vms:
641+
self.proxmox_vms[vm_setting['name']] = {}
642+
643+
self.proxmox_vms[vm_setting['name']]['vmid'] = vm_setting['vmid']
644+
self.proxmox_vms[vm_setting['name']]['node'] = proxmox_node
645+
except ResourceException as e:
646+
raise RuntimeError(f"Proxmox API error: {e}") from e
647+
except requests.exceptions.ConnectionError:
648+
raise RuntimeError("Failed to connect to Proxmox API")
649+
except requests.exceptions.HTTPError as e:
650+
status = e.response.status_code
651+
raise RuntimeError(f"HTTP {status}: {e.response.text}") from e
652+
653+
654+
def __get_proxmox_lxcs(self):
655+
try:
656+
for proxmox_node in self.proxmox_nodes:
657+
all_lxc_settings = self.proxmox_api.nodes(proxmox_node).get('lxc')
658+
for lxc_setting in all_lxc_settings:
659+
if 'template' in lxc_setting and lxc_setting['template'] == 1:
660+
continue
661+
662+
if not lxc_setting['name'] in self.proxmox_lxc:
663+
self.proxmox_lxc[lxc_setting['name']] = {}
664+
665+
self.proxmox_lxc[lxc_setting['name']]['vmid'] = lxc_setting['vmid']
666+
self.proxmox_lxc[lxc_setting['name']]['node'] = proxmox_node
667+
except ResourceException as e:
668+
raise RuntimeError(f"Proxmox API error: {e}") from e
669+
except requests.exceptions.ConnectionError:
670+
raise RuntimeError("Failed to connect to Proxmox API")
671+
except requests.exceptions.HTTPError as e:
672+
status = e.response.status_code
673+
raise RuntimeError(f"HTTP {status}: {e.response.text}") from e
674+
675+
676+
def __wait_for_migration_task(self, proxmox_node: str, proxmox_task_id: int):
677+
try:
678+
start_time = int(time.time())
679+
680+
while True:
681+
current_time = int(time.time())
682+
elapsed_seconds = current_time - start_time
683+
684+
if elapsed_seconds >= 600: # 10 minutes
685+
raise ValueError(f"Unable to complete task {proxmox_task_id} in defined time")
686+
687+
task_status = self.proxmox_api.nodes(proxmox_node).tasks(proxmox_task_id).status.get()
688+
689+
if task_status['status'] == 'stopped':
690+
if 'exitstatus' in task_status and task_status['exitstatus'] == 'OK':
691+
break
692+
else:
693+
return 500, {'result': f"Task {proxmox_task_id} is stopped but exit status does not appear to be successful: {task_status['exit_status']}"}
694+
except ResourceException as e:
695+
return 500, {'content': f"Proxmox API error: {e}"}
696+
except requests.exceptions.ConnectionError as e:
697+
return 500, {'content': f"Failed to connect to Proxmox API: {e}"}
698+
except requests.exceptions.HTTPError as e:
699+
status = e.response.status_code
700+
return 500, {'content': f"HTTP {status}: {e.response.text}"}
701+
702+
703+
def migrate_vm(self, proxmox_vmid: int, proxmox_node: str, proxmox_target_node: str):
704+
migrate_vm_data = {
705+
'target': proxmox_target_node,
706+
'online': 1
707+
}
708+
709+
try:
710+
migrate_vm_task_id = self.proxmox_api.nodes(proxmox_node).qemu(proxmox_vmid).migrate.post(**migrate_vm_data)
711+
self.__wait_for_migration_task(proxmox_node, migrate_vm_task_id)
712+
713+
return 200, {'result': f"VM (vmid: {proxmox_vmid}) has been migrated to node {proxmox_target_node}"}
714+
except ResourceException as e:
715+
return 500, {'result': f"Proxmox API error: {e}"}
716+
except requests.exceptions.ConnectionError:
717+
return 500, {'result': f"Failed to connect to Proxmox API: {e}"}
718+
except requests.exceptions.HTTPError as e:
719+
status = e.response.status_code
720+
return 500, {'result': f"HTTP {status}: {e.response.text}"}
721+
722+
723+
def migrate_lxc(self, proxmox_vmid: int, proxmox_node: str, proxmox_target_node: str):
724+
migrate_lxc_data = {
725+
'target': proxmox_target_node,
726+
'online': 1
727+
}
728+
729+
try:
730+
migrate_lxc_task_id = self.proxmox_api.nodes(proxmox_node).lxc(proxmox_vmid).migrate.post(**migrate_lxc_data)
731+
self.__wait_for_migration_task(proxmox_node, migrate_lxc_task_id)
732+
return 200, {'result': f"LXC (vmid: {proxmox_vmid}) has been migrated to node {proxmox_target_node}"}
733+
except ResourceException as e:
734+
return 500, {'result': f"Proxmox API error: {e}"}
735+
except requests.exceptions.ConnectionError:
736+
return 500, {'result': f"Failed to connect to Proxmox API: {e}"}
737+
except requests.exceptions.HTTPError as e:
738+
status = e.response.status_code
739+
return 500, {'result': f"HTTP {status}: {e.response.text}"}
740+
741+

0 commit comments

Comments
 (0)