Skip to content

Repository files navigation

WhereIsMyVM

Language: English | Français

https://whereismyvm.com

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.

Features

  • ESXi inventory through pyVmomi.
  • Proxmox VE inventory through proxmoxer.
  • Read-only access to hypervisors.
  • Manual Force refresh state button.
  • 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_inventory management command.
  • Docker Compose deployment with Python 3.12, Django/Gunicorn, and MariaDB.

Architecture

The main Django project is whereismyvm.

The application code lives in allvm.

Main components:

  • allvm/services/hypervisor.py: reads hypervisor.yaml and 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.

Configuration Files

The following files must exist at the project root but must not be committed:

  • hypervisor.yaml
  • config.yaml
  • .env for Docker/production deployments

They are ignored by Git and excluded from Docker images.

Hypervisor Configuration

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: password

For 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.

Application Configuration

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: 150

lowram alerts when host free RAM is below the configured GB value. lowdisk alerts when host free storage is below the configured GB value.

Recommended Local Development With Docker Compose

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.yaml

Edit .env, hypervisor.yaml, and config.yaml, then start the stack:

docker compose up -d --build

The 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_inventory

Useful Docker commands:

docker compose logs -f web
docker compose logs -f db
docker compose restart web
docker compose down

When 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.

Alternative Local Python Virtualenv

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
deactivate

On Windows PowerShell, activate the virtualenv with:

.\venv\Scripts\Activate.ps1

Docker Compose Deployment

The 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.yaml and config.yaml;
  • Apache on the host as reverse proxy.

Create the environment file:

cp .env.sample .env

Edit .env, hypervisor.yaml, and config.yaml.

Minimum .env values to change before production:

  • DJANGO_SECRET_KEY
  • DJANGO_ALLOWED_HOSTS
  • DJANGO_CSRF_TRUSTED_ORIGINS
  • WHEREISMYVM_DB_PASSWORD
  • MARIADB_ROOT_PASSWORD

Start the stack:

docker compose up -d --build

Daily 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 web

The 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_inventory

If 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 migrate when RUN_MIGRATIONS=1;
  • python manage.py collectstatic --noinput when RUN_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 createsuperuser

Then 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.sql

For daily automated backups with retention, use the provided script:

bash scripts/backup_mariadb.sh

The retention depth is configured at the top of the script:

BACKUP_RETENTION_DAYS=30

By 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>&1

Restore it with:

docker compose exec -T db mariadb -u root -p whereismyvm < whereismyvm_backup.sql

Also 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:ro

More details are available in docs/deployment-docker.md.

Apache Reverse Proxy

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.

Scheduled Inventory Refresh

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_inventory

With 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>&1

This 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.

VM Tags

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.

PDF Report

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.

Extending Hypervisor Support

To add another hypervisor type:

  1. Implement HypervisorServiceInterface.
  2. Return normalized HypervisorData.
  3. Register the new service in HypervisorServiceFactory.

Use allvm/services/esxi.py and allvm/services/proxmox.py as examples.

Troubleshooting

Force refresh state returns HTTP 403

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 web

Look 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.

pip install -r requirements.txt fails on PyYAML

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.

pip install -r requirements.txt fails on lxml

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.txt

RHEL / Rocky Linux / AlmaLinux / CentOS with dnf:

sudo dnf install -y gcc python3-devel libxml2-devel libxslt-devel zlib-devel
pip install -r requirements.txt

Older CentOS with yum:

sudo yum install -y gcc python3-devel libxml2-devel libxslt-devel zlib-devel
pip install -r requirements.txt

Docker deployments already install the required system dependencies inside the image, so this issue mainly affects manual virtualenv installations on Linux or old dependency pins.

Documentation

Project documentation lives in docs.

Important files:

About

All your VM at one place!

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages