Language: English | Français
WhereIsMyVM is a Django application that inventories ESXi and Proxmox VE hypervisors in read-only mode.
It connects to configured hypervisors, discovers hosts and VMs, stores the latest known inventory in a local database, and displays it in a web dashboard. It can also generate a PDF report from the persisted inventory, with a live fallback before the first refresh.
- ESXi inventory through
pyVmomi. - Proxmox VE inventory through
proxmoxer. - Read-only access to hypervisors.
- Manual
Force refresh statebutton. - Progress bar during refresh.
- Persisted latest inventory state.
- Local colored VM tags managed in Django admin and assignable from the main dashboard.
- Tag and power filters, plus text search on VM, IP, OS, hypervisor, and node.
- Refresh change history and comparison between two refreshes.
- VM detail page with persisted metadata, tags, dates, and recent changes.
- PDF generation and email sending.
- Cron-friendly
refresh_inventorymanagement command. - Docker Compose deployment with Python 3.12, Django/Gunicorn, and MariaDB.
The main Django project is whereismyvm.
The application code lives in allvm.
Main components:
allvm/services/hypervisor.py: readshypervisor.yamland dispatches to the right hypervisor service.allvm/services/esxi.py: ESXi inventory service.allvm/services/proxmox.py: Proxmox VE inventory service.allvm/services/inventory.py: persists and reloads inventory state and refresh changes.allvm/models.py: Django models for hypervisors, VMs, inventory state, refresh history, and tags.allvm/templates/allvm/index.html: main dashboard.allvm/templates/allvm/vm_detail.html: VM detail page.allvm/templates/allvm/changes.html: refresh comparison page.
The web dashboard reads from the database. The refresh button performs a live inventory and updates the database.
The PDF endpoint uses the persisted inventory when at least one refresh has been completed. Before the first refresh, it falls back to a live hypervisor query and only uses the database to enrich live VMs with local tags when they match an already known VM.
The following files must exist at the project root but must not be committed:
hypervisor.yamlconfig.yaml.envfor Docker/production deployments
They are ignored by Git and excluded from Docker images.
Create hypervisor.yaml from hypervisor.yaml.sample.
Example:
hypervisor:
-
host: proxmox1.example.com:8006
type: proxmox
node_name: pve-node-1
login: user@pam
passwd: password
-
host: esxi1.example.com
type: esxi
legacy_ssl: true # Optional. Enable only for old ESXi hosts such as ESXi 5.5.
login: readonly-user
passwd: passwordFor Proxmox clusters, node_name identifies the node. If it is not provided, the first node returned by the API is used.
Use read-only accounts on both ESXi and Proxmox.
For old ESXi hosts, especially ESXi 5.5, legacy_ssl: true enables legacy OpenSSL ciphers for that host only. Keep it disabled for recent ESXi hosts.
Create config.yaml from config.yaml.sample.
Example:
mail_from: 'no-reply@example.com'
mail_recipients:
- admin@example.com
smtp_host: 'localhost'
smtp_port: '25'
lowram: 20
lowdisk: 150lowram alerts when host free RAM is below the configured GB value.
lowdisk alerts when host free storage is below the configured GB value.
Docker Compose is the recommended local development workflow. It runs the application with the same building blocks as production: Python 3.12, Django/Gunicorn, and MariaDB.
Create the local configuration files:
cp .env.sample .env
cp hypervisor.yaml.sample hypervisor.yaml
cp config.yaml.sample config.yamlEdit .env, hypervisor.yaml, and config.yaml, then start the stack:
docker compose up -d --buildThe application is then available at:
http://127.0.0.1:8000/
Django admin login page:
http://127.0.0.1:8000/admin/
Run Django management commands inside the web container:
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py test
docker compose exec web python manage.py refresh_inventoryUseful Docker commands:
docker compose logs -f web
docker compose logs -f db
docker compose restart web
docker compose downWhen using Docker Compose, do not run python manage.py ... directly on the host unless dependencies are installed in a host virtualenv. Prefer docker compose exec web python manage.py ....
If another local development server already uses port 8000, stop it before starting Docker Compose, or change the published port in docker-compose.yml.
A host virtualenv can still be useful for quick Python checks or debugging outside Docker.
Python 3.12 is the recommended runtime.
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
deactivateOn Windows PowerShell, activate the virtualenv with:
.\venv\Scripts\Activate.ps1The recommended production deployment uses:
- one Python 3.12 Django/Gunicorn container;
- one MariaDB container;
- a persistent MariaDB Docker volume;
- a persistent static files Docker volume;
- bind mounts for
hypervisor.yamlandconfig.yaml; - Apache on the host as reverse proxy.
Create the environment file:
cp .env.sample .envEdit .env, hypervisor.yaml, and config.yaml.
Minimum .env values to change before production:
DJANGO_SECRET_KEYDJANGO_ALLOWED_HOSTSDJANGO_CSRF_TRUSTED_ORIGINSWHEREISMYVM_DB_PASSWORDMARIADB_ROOT_PASSWORD
Start the stack:
docker compose up -d --buildDaily Docker commands:
# Start or rebuild the application and MariaDB
docker compose up -d --build
# Follow Django/Gunicorn logs
docker compose logs -f web
# Follow MariaDB logs
docker compose logs -f db
# Stop containers without deleting MariaDB data
docker compose down
# Restart only the web container
docker compose restart webThe application is then available at:
http://127.0.0.1:8000/
Django management commands should be run inside the web container:
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py test
docker compose exec web python manage.py refresh_inventoryIf another local development server already uses port 8000, stop it before starting Docker Compose, or change the published port in docker-compose.yml.
The web container waits for MariaDB before starting. If enabled in .env, it also runs:
python manage.py migratewhenRUN_MIGRATIONS=1;python manage.py collectstatic --noinputwhenRUN_COLLECTSTATIC=1.
Static files are served by Django/Gunicorn through WhiteNoise. Apache can therefore keep proxying the whole application to http://127.0.0.1:8000/; no separate Apache Alias /static/ is required for the default Docker deployment.
python manage.py migrate does not overwrite an existing database. Django stores the applied migration history in the django_migrations table. When migrate is run again, Django only applies migrations that have not already been applied.
It is safe to keep RUN_MIGRATIONS=1 in production, but it is still recommended to back up MariaDB before deploying an application upgrade that includes new migrations.
Create the admin user:
docker compose exec web python manage.py createsuperuserThen open the Django admin login page:
http://127.0.0.1:8000/admin/
Behind Apache, replace 127.0.0.1:8000 with your production domain, for example:
https://whereismyvm.example.com/admin/
The Compose stack stores MariaDB data in the Docker volume mariadb-data, mounted at /var/lib/mysql in the database container.
Application static files are collected into the Docker volume static-data, mounted at /app/staticfiles in the web container.
Back up the MariaDB database manually with:
docker compose exec db mariadb-dump -u root -p whereismyvm > whereismyvm_backup.sqlFor daily automated backups with retention, use the provided script:
bash scripts/backup_mariadb.shThe retention depth is configured at the top of the script:
BACKUP_RETENTION_DAYS=30By default, backup files are written to backups/mariadb/ and are ignored by Git.
Example cron entry for a daily backup at 03:00:
0 3 * * * cd /opt/whereismyvm && bash scripts/backup_mariadb.sh >> /var/log/whereismyvm-db-backup.log 2>&1Restore it with:
docker compose exec -T db mariadb -u root -p whereismyvm < whereismyvm_backup.sqlAlso back up .env, config.yaml, and hypervisor.yaml, because they are intentionally stored outside the Docker image.
Configuration files stay outside the image:
volumes:
- ./hypervisor.yaml:/app/hypervisor.yaml:ro
- ./config.yaml:/app/config.yaml:roMore details are available in docs/deployment-docker.md.
The Docker Compose file exposes Gunicorn only on localhost:
127.0.0.1:8000
Apache can proxy to:
http://127.0.0.1:8000/
See docs/deployment-docker.md for HTTP and HTTPS examples.
WhereIsMyVM becomes much more useful when inventory refreshes are automated. A scheduled refresh keeps the database up to date without requiring a user to open the dashboard and click Force refresh state.
Use the Django management command:
python manage.py refresh_inventoryWith Docker Compose, schedule it from Linux cron:
0 2 * * * cd /opt/whereismyvm && docker compose exec -T web python manage.py refresh_inventory >> /var/log/whereismyvm-refresh.log 2>&1This example refreshes the inventory every day at 02:00. The web dashboard, tag filters, VM detail pages, change history, and cached PDF report will then use the latest persisted inventory.
Tags are local to WhereIsMyVM. Native Proxmox, VMware, or vCenter tags are not read or synchronized.
Tags are created and edited in Django admin. Existing active tags can be associated with or removed from discovered VMs directly from the main dashboard.
Each tag has:
- a name;
- an HTML color;
- an optional description;
- an active/inactive status.
Open:
/pdf/
This endpoint generates a PDF and sends it to the configured recipients.
When a persisted inventory exists, the PDF is generated from the latest database state and includes local VM tags.
If no refresh has ever been completed, the endpoint falls back to live hypervisor queries and enriches matching VMs with local tags when possible.
To add another hypervisor type:
- Implement
HypervisorServiceInterface. - Return normalized
HypervisorData. - Register the new service in
HypervisorServiceFactory.
Use allvm/services/esxi.py and allvm/services/proxmox.py as examples.
The refresh button sends a POST request protected by Django CSRF checks. If the request is rejected, WhereIsMyVM logs the CSRF failure reason and useful request context in the web container logs:
docker compose logs -f webLook for a line starting with CSRF failure. It includes the rejection reason, host, origin, referer, detected scheme, X-Forwarded-Proto, user agent, and remote address.
Common causes are an HTTPS reverse proxy that does not forward X-Forwarded-Proto: https, or a public URL missing from DJANGO_CSRF_TRUSTED_ORIGINS.
Older dependency pins such as PyYAML==5.4.1 are not compatible with Python 3.12 and can fail with an error similar to:
AttributeError: 'build_ext' object has no attribute 'cython_sources'
Use the current requirements.txt, which pins PyYAML==6.0.3.
If pip install -r requirements.txt fails with an error similar to:
Error: Please make sure the libxml2 and libxslt development packages are installed.
ERROR: Failed to build 'lxml'
install the required system packages, then run pip install -r requirements.txt again.
Debian / Ubuntu:
sudo apt update
sudo apt install -y build-essential python3-dev libxml2-dev libxslt1-dev zlib1g-dev
pip install -r requirements.txtRHEL / Rocky Linux / AlmaLinux / CentOS with dnf:
sudo dnf install -y gcc python3-devel libxml2-devel libxslt-devel zlib-devel
pip install -r requirements.txtOlder CentOS with yum:
sudo yum install -y gcc python3-devel libxml2-devel libxslt-devel zlib-devel
pip install -r requirements.txtDocker deployments already install the required system dependencies inside the image, so this issue mainly affects manual virtualenv installations on Linux or old dependency pins.
Project documentation lives in docs.
Important files: