This commit is contained in:
Holger Sielaff
2025-08-27 09:55:55 +02:00
commit 90c0ff61ed
107 changed files with 8535 additions and 0 deletions

0
proxmox/__init__.py Normal file
View File

79
proxmox/admin.py Normal file
View File

@@ -0,0 +1,79 @@
import logging
from django.contrib import admin
from django.db.models import Q
from lib.decorators import readonly
from lib.proxmox import Proxmox
from proxmox.models import Lxc, LxcTemplate
@readonly
def sync_all_lxc_templates_from_proxmox(*args, **kwargs):
pm = Proxmox()
for storage in pm.storage_get(enabled=1):
logging.debug(f'Syncing Templates from storage {storage["storage"]}')
storage_name = storage['storage']
for tmpl in pm.storage(f'{storage_name}/content').get(content='vztmpl'):
try:
logging.debug(f'Updating {tmpl["volid"]}')
template = LxcTemplate.objects.get(volid=tmpl['volid'])
except LxcTemplate.DoesNotExist:
logging.debug(f'Fail - Creating {tmpl["volid"]}')
template = LxcTemplate.objects.create(volid=tmpl['volid'])
template.write(**tmpl)
@readonly
def sync_all_lxc_from_proxmox(*args, **kwargs):
from lib.proxmox import Proxmox
pm = Proxmox()
existing_vms = []
for lxc_data in pm.get_all_lxc(as_dict=True, **kwargs):
vmid = lxc_data.pop('vmid')
try:
lx: Lxc = Lxc.objects.get(vmid=vmid)
logging.info(f'Updating {vmid}')
except Lxc.DoesNotExist:
logging.info(f'Creating {vmid}')
lx = Lxc.objects.create(**{'vmid': vmid})
lx.from_proxmox(**lxc_data)
existing_vms.append(vmid)
to_delete = Lxc.objects.filter(~Q(vmid__in=existing_vms))
if to_delete:
logging.info(f'Deleting {[d.vmid for d in to_delete]}')
to_delete.delete()
sync_all_lxc_from_proxmox.short_description = 'Sync all LXC from Proxmox'
@admin.register(Lxc)
class LxcAdmin(admin.ModelAdmin):
actions = [sync_all_lxc_from_proxmox, 'sync_selected_from_proxmox']
search_fields = ('name', 'vmid', 'hostname', 'hwaddr')
list_display = ('name', 'vmid', 'hwaddr', 'disksize', 'memory', 'cpus', 'status')
list_filter = ('status', 'disksize',)
def get_readonly_fields(self, request, obj=None):
if obj:
return [k.name for k in obj._meta.fields if
k.name not in ['name', 'cores', 'hwaddr', 'size', 'cpus', 'memory', 'description', 'hostname']]
return self.readonly_fields
@admin.action(description='Sync selected from proxmox')
@readonly
def sync_selected_from_proxmox(self, request, queryset):
for lx in queryset:
lx.sync_from_proxmox()
@admin.register(LxcTemplate)
class LxcTemplateAdmin(admin.ModelAdmin):
actions = [sync_all_lxc_templates_from_proxmox]
search_fields = ('volid',)
list_display = ('volid', 'human_size', 'content')

6
proxmox/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ProxmoxConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'proxmox'

View File

@@ -0,0 +1,80 @@
# Generated by Django 5.2.4 on 2025-07-07 11:19
import django.core.validators
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Lxc',
fields=[
('internal_id', models.BigAutoField(primary_key=True, serialize=False)),
('vmid', models.IntegerField(blank=True, null=True, unique=True)),
('name', models.CharField(blank=True, default='', max_length=150, null=True, verbose_name='Container Name')),
('hostname', models.CharField(blank=True, default='', max_length=150, null=True)),
('hwaddr', models.CharField(blank=True, default=uuid.uuid4, max_length=150, null=True, unique=True)),
('size', models.CharField(blank=True, max_length=4, null=True)),
('cores', models.IntegerField(blank=True, default=1, null=True)),
('memory', models.IntegerField(default=512, help_text='in MB', validators=[django.core.validators.MinValueValidator(128)])),
('disksize', models.IntegerField(default=12, help_text='in GB', validators=[django.core.validators.MinValueValidator(8)])),
('swap', models.IntegerField(blank=True, null=True)),
('description', models.TextField(blank=True, default='', null=True)),
('cpus', models.IntegerField(blank=True, default=1, null=True, validators=[django.core.validators.MinValueValidator(1)])),
('uptime', models.IntegerField(blank=True, null=True)),
('maxswap', models.IntegerField(blank=True, null=True)),
('cpu', models.IntegerField(blank=True, null=True)),
('disk', models.IntegerField(blank=True, null=True)),
('netout', models.IntegerField(blank=True, null=True)),
('diskwrite', models.IntegerField(blank=True, null=True)),
('diskread', models.IntegerField(blank=True, null=True)),
('pid', models.IntegerField(blank=True, null=True)),
('maxdisk', models.IntegerField(blank=True, null=True)),
('mem', models.IntegerField(blank=True, null=True)),
('maxmem', models.IntegerField(blank=True, null=True)),
('netin', models.IntegerField(blank=True, null=True)),
('status', models.CharField(blank=True, default='', max_length=150, null=True)),
('type', models.CharField(blank=True, default='', max_length=150, null=True)),
('onboot', models.CharField(blank=True, default='', max_length=150, null=True)),
('nameserver', models.CharField(blank=True, default='', max_length=150, null=True)),
('digest', models.CharField(blank=True, default='', max_length=150, null=True)),
('rootfs', models.CharField(blank=True, default='', max_length=150, null=True)),
('arch', models.CharField(blank=True, default='', max_length=150, null=True)),
('ostype', models.CharField(blank=True, default='', max_length=150, null=True)),
('net0', models.CharField(blank=True, default='', max_length=150, null=True)),
('features', models.CharField(blank=True, default='', max_length=250, null=True)),
('snaptime', models.CharField(blank=True, default='', max_length=150, null=True)),
('parent', models.CharField(blank=True, default='', max_length=150, null=True)),
('tags', models.CharField(blank=True, default='', max_length=250, null=True)),
('console', models.CharField(blank=True, default='', max_length=150, null=True)),
('tty', models.CharField(blank=True, default='', max_length=150, null=True)),
('searchdomain', models.CharField(blank=True, default='', max_length=150, null=True)),
('unprivileged', models.CharField(blank=True, default='', max_length=10, null=True)),
('lxc', models.CharField(blank=True, default='', max_length=10, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LxcTemplate',
fields=[
('internal_id', models.BigAutoField(primary_key=True, serialize=False)),
('volid', models.CharField(max_length=150, unique=True)),
('ctime', models.IntegerField(default=0)),
('size', models.IntegerField(default=0)),
('format', models.CharField(max_length=10)),
('content', models.CharField(default='tgz', max_length=10)),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.4 on 2025-07-08 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='lxc',
name='disk',
field=models.CharField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='diskread',
field=models.CharField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='diskwrite',
field=models.CharField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='netout',
field=models.CharField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='uptime',
field=models.CharField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-07-08 11:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0002_alter_lxc_disk_alter_lxc_diskread_and_more'),
]
operations = [
migrations.AlterField(
model_name='lxc',
name='disksize',
field=models.IntegerField(default=12, help_text='in GB'),
),
migrations.AlterField(
model_name='lxc',
name='memory',
field=models.IntegerField(default=512, help_text='in MB'),
),
]

View File

@@ -0,0 +1,74 @@
# Generated by Django 5.2.4 on 2025-07-08 11:29
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0003_alter_lxc_disksize_alter_lxc_memory'),
]
operations = [
migrations.AlterField(
model_name='lxc',
name='cores',
field=models.BigIntegerField(blank=True, default=1, null=True),
),
migrations.AlterField(
model_name='lxc',
name='cpu',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='cpus',
field=models.BigIntegerField(blank=True, default=1, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AlterField(
model_name='lxc',
name='disksize',
field=models.BigIntegerField(default=12, help_text='in GB'),
),
migrations.AlterField(
model_name='lxc',
name='maxdisk',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='maxmem',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='maxswap',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='mem',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='memory',
field=models.BigIntegerField(default=512, help_text='in MB'),
),
migrations.AlterField(
model_name='lxc',
name='netin',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='pid',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='lxc',
name='swap',
field=models.BigIntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-08 14:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0004_alter_lxc_cores_alter_lxc_cpu_alter_lxc_cpus_and_more'),
]
operations = [
migrations.AlterField(
model_name='lxc',
name='lxc',
field=models.CharField(blank=True, default='', max_length=150, null=True),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.4 on 2025-07-21 11:03
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0005_alter_lxc_lxc'),
]
operations = [
migrations.AddField(
model_name='lxc',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='lxc',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='lxctemplate',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='lxctemplate',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-07-22 13:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proxmox', '0006_lxc_created_at_lxc_updated_at_lxctemplate_created_at_and_more'),
]
operations = [
migrations.AddField(
model_name='lxctemplate',
name='net0',
field=models.CharField(blank=True, default='name=eth0,bridge=vmbr0,firewall=0,ip=dhcp', max_length=150, null=True),
),
migrations.AlterField(
model_name='lxc',
name='net0',
field=models.CharField(blank=True, default='name=eth0,bridge=vmbr0,firewall=0,ip=dhcp', max_length=150, null=True),
),
]

View File

354
proxmox/models.py Normal file
View File

@@ -0,0 +1,354 @@
import logging
import re
from uuid import uuid4
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django_proxmox_mikrotik.configs import ProxmoxConfig
from lib import human_size
from lib.db import BaseModel, TaskAwareModelMixin
from lib.decorators import skip_signal
from lib.proxmox import Proxmox, get_comma_separated_values
from manager.models import DevContainer
no_int_re = re.compile(r'[^\d]')
class ProxmoxAbstractModel(BaseModel, TaskAwareModelMixin):
class Meta:
abstract = True
@property
def change_map(self) -> dict:
raise NotImplemented('@property change_map" not implemented')
@property
def comma_separated_values(self) -> list:
"""A list of fields that are comma separated values (X=1,Y=2,...
"""
return []
@property
def no_csv_overwrite(self) -> list:
"""Maybe a value within a csv options
has the same name than a non-csv option.
Ommit those"""
return []
@property
def non_proxmox_values(self) -> list:
"""A list of fields that are not in proxmox, but in db
Ao so not sync directly (maybe as csv)"""
return []
def to_proxmox(self):
raise NotImplemented('Not implemented')
def sync_from_proxmox(self):
raise NotImplemented('Not implemented')
@property
def csv_field_map(self):
return {}
@property
def int_fields(self):
return []
def from_proxmox(self, **kwargs):
"""Sets the values in DB
Extracts csv Values into self, if given"""
logging.debug(f"Got {kwargs} from proxmox")
kwkeys = list(kwargs.keys())
params = {}
for k in kwkeys:
logging.info(f"Process {k} with value {kwargs[k]}")
v = kwargs[k]
if not hasattr(self, k):
continue
if k in self.comma_separated_values:
_vals = get_comma_separated_values(v)
logging.debug(f"Got {_vals}")
for _k, _v in _vals.items():
if _k in self.no_csv_overwrite:
logging.debug(f"{_k} is in no_csv_overwrite - omitting")
continue
csvkey, selfkey, csvfun = self.csv_field_map.get(k, (_k, _k, lambda x: x))
if hasattr(self, selfkey):
if csvfun:
_v = csvfun(_v)
elif selfkey in self.int_fields:
_v = int(no_int_re.sub('', _v) or 0)
logging.debug(f"Update {selfkey} to {_v}")
params[selfkey] = _v
else:
logging.info(f"{selfkey} not found in {type(self)}")
else:
if isinstance(getattr(self, k), models.IntegerField):
v = no_int_re.sub('', _v) or 0
params[k] = v
logging.debug(f"No CSValues for {self}")
return self.write(**params)
class Lxc(ProxmoxAbstractModel):
@property
def int_fields(self):
return ['cores', 'memory', 'disksize']
@property
def comma_separated_values(self):
return ['net0', 'rootfs', 'features']
@property
def no_csv_overwrite(self):
return ['name']
@property
def non_proxmox_values(self):
return ['vmid', 'hwaddr', 'disksize']
@property
def csv_field_map(self):
return {
'rootfs': ('size', 'disksize', lambda x: int(no_int_re.sub('', x) or 0)),
}
@property
def proxmox_console_url(self):
return (
f"https://{ProxmoxConfig.HOST}:8006/"
f"?console=lxc&xtermjs=1&vmid={self.vmid}"
f"&vmname={self.hostname}&node={ProxmoxConfig.NODE}&cmd="
)
def delete(self, task=None):
with Proxmox() as pm:
try:
if task:
task.wrap_proxmox_function(self.stop)
else:
self.stop()
except Exception as e:
if 'running' in str(e):
logging.info(f"Could not stop {self.vmid} - {e}")
else:
raise
try:
if task:
task.wrap_proxmox_function(pm.lxc_delete, self.vmid, force=1, purge=1)
else:
result = pm.lxc_delete(self.vmid, force=1, purge=1)
logging.info(f"Deleted {self.vmid} from proxmox - {result}")
except Exception as e:
logging.error(f"Could not delete {self.vmid} from proxmox", e)
finally:
super().delete()
vmid = models.IntegerField(null=True, blank=True, unique=True)
name = models.CharField(max_length=150, null=True, blank=True, default='', verbose_name='Container Name')
hostname = models.CharField(max_length=150, null=True, blank=True, default='', )
# This one is from net0
hwaddr = models.CharField(max_length=150, null=True, blank=True, default=uuid4, unique=True)
# this comes from rootfs
size = models.CharField(max_length=4, null=True, blank=True)
cores = models.BigIntegerField(null=True, blank=True, default=1)
memory = models.BigIntegerField(default=512, help_text='in MB', ) # validators=[MinValueValidator(128)])
disksize = models.BigIntegerField(default=12, help_text='in GB', ) # validators=[MinValueValidator(8)])
swap = models.BigIntegerField(null=True, blank=True)
description = models.TextField(null=True, blank=True, default='')
cpus = models.BigIntegerField(null=True, blank=True, validators=[MinValueValidator(1)], default=1)
uptime = models.CharField(null=True, blank=True)
maxswap = models.BigIntegerField(null=True, blank=True)
cpu = models.BigIntegerField(null=True, blank=True)
disk = models.CharField(null=True, blank=True)
netout = models.CharField(null=True, blank=True)
diskwrite = models.CharField(null=True, blank=True)
diskread = models.CharField(null=True, blank=True)
pid = models.BigIntegerField(null=True, blank=True)
maxdisk = models.BigIntegerField(null=True, blank=True)
mem = models.BigIntegerField(null=True, blank=True)
maxmem = models.BigIntegerField(null=True, blank=True)
netin = models.BigIntegerField(null=True, blank=True)
status = models.CharField(max_length=150, null=True, blank=True, default='')
type = models.CharField(max_length=150, null=True, blank=True, default='')
onboot = models.CharField(max_length=150, null=True, blank=True, default='')
nameserver = models.CharField(max_length=150, null=True, blank=True, default='')
digest = models.CharField(max_length=150, null=True, blank=True, default='')
rootfs = models.CharField(max_length=150, null=True, blank=True, default='')
arch = models.CharField(max_length=150, null=True, blank=True, default='')
ostype = models.CharField(max_length=150, null=True, blank=True, default='')
net0 = models.CharField(max_length=150, null=True, blank=True, default='name=eth0,bridge=vmbr0,firewall=0,ip=dhcp')
features = models.CharField(max_length=250, null=True, blank=True, default='')
snaptime = models.CharField(max_length=150, null=True, blank=True, default='')
parent = models.CharField(max_length=150, null=True, blank=True, default='')
tags = models.CharField(max_length=250, null=True, blank=True, default='')
console = models.CharField(max_length=150, null=True, blank=True, default='')
tty = models.CharField(max_length=150, null=True, blank=True, default='')
searchdomain = models.CharField(max_length=150, null=True, blank=True, default='')
unprivileged = models.CharField(max_length=10, null=True, blank=True, default='')
lxc = models.CharField(max_length=150, null=True, blank=True, default='')
def __str__(self):
return f'{self.name} ({self.vmid})'
def sync_from_proxmox(self):
pm = Proxmox()
try:
data = pm.lxc_get(f'{self.vmid}/config')
logging.debug(f"Got raw data '{data}' from proxmox")
if not data:
logging.warning(f'Could not find {self.vmid} in proxmox - deleting from local database!')
return self.delete()
self.from_proxmox(**data)
super().save()
except Exception as e:
logging.error(f"Could not get config for {self.vmid} - {e}")
return self
@property
def _ch_disksize(self):
if self._old_values['disksize'] == self.disksize:
return False
if self.disksize > 100:
logging.warning(f'disksize is > 100')
return False
if self.disksize < self._old_values['disksize']:
logging.warning(f"Can not shrink disksize")
return False
return True
def change_disksize(self):
"""Just to disable some magick at the moment"""
if self._ch_disksize:
route = f'{self.vmid}/resize'
args = {
'disk': 'rootfs',
'size': f'{self.disksize}G',
}
with Proxmox() as pm:
try:
result = pm.lxc_put(route, **args)
logging.info(f"Changed disksize for container {self.vmid} to {self.disksize}G - {result}")
logs = pm.get_task_status(taskhash=result)
logging.debug(f"Tasklog for {self.vmid} is {logs}")
except Exception as e:
logging.error(f"Could not change disksize for container {self.vmid} to {self.disksize}G - {e}")
return self
def change_memory(self):
"""Just to disable some magick at the moment"""
if self._old_values['memory'] != self.memory:
route = f'{self.vmid}/config'
args = {
'memory': self.memory,
}
with Proxmox() as pm:
try:
result = pm.lxc_put(route, **args)
logging.info(f"Changed memory for container {self.vmid} to {self.memory}MB - {result}")
except Exception as e:
self.memory = self._old_values['memory']
super().save(update_fields=['memory'])
logging.error(f"Could not change memory for container {self.vmid} to {self.memory}MB - {e}")
return self
def change_cores(self):
if self._old_values['cores'] != self.cores:
logging.debug(f"Changing cores for {self.vmid} to {self.cores}")
route = f'{self.vmid}/config'
args = {
'cores': self.cores,
}
with Proxmox() as pm:
try:
result = pm.lxc_put(route, **args)
logging.info(f"Changed cores for container {self.vmid} to {self.cores} - {result}")
return result
except Exception as e:
self.cores = self._old_values['cores']
super().save(update_fields=['cores'])
logging.error(f"Could not change cores for container {self.vmid} to {self.cores} - {e}")
return None
def start(self):
startresult = self._start_stop_actions('start')
if startresult:
self.status = 'running'
return startresult
def stop(self):
stopresult = self._start_stop_actions('stop')
if stopresult:
self.status = 'stopped'
return stopresult
def reboot(self):
rebootresult = self._start_stop_actions('reboot')
if rebootresult:
self.status = 'running'
return rebootresult
def shutdown(self):
shresult = self._start_stop_actions('shutdown')
if shresult:
self.status = 'stopped'
return shresult
def _start_stop_actions(self, action):
assert action in ('start', 'stop', 'shutdown', 'reboot')
with Proxmox() as pm:
try:
result = pm.lxc_post(f'{self.vmid}/status/{action}')
logging.info(f"{action}ed {self.vmid} - {result}")
return result
except Exception as e:
logging.error(f"Could not {action} {self.vmid} - {e}")
return False
def save(self, *args, **kwargs):
logging.debug(f"Saving {self}")
super().save(*args, **kwargs)
def to_proxmox(self):
self.change_disksize()
self.change_memory()
self.change_cores()
@receiver(pre_save, sender=Lxc)
@skip_signal()
def pre_save_lxc(sender, instance: Lxc, **kwargs):
instance.hwaddr = str(instance.hwaddr or uuid4()).upper()
if instance._state.adding:
logging.info(f'Created {instance} via post_save event - do nothing, must be done via CloneContainer')
return
instance.change_disksize()
instance.change_memory()
instance.change_cores()
class LxcTemplate(ProxmoxAbstractModel, TaskAwareModelMixin):
volid = models.CharField(max_length=150, unique=True)
ctime = models.IntegerField(default=0)
size = models.IntegerField(default=0)
format = models.CharField(max_length=10)
content = models.CharField(max_length=10, default='tgz')
net0 = models.CharField(max_length=150, null=True, blank=True, default='name=eth0,bridge=vmbr0,firewall=0,ip=dhcp')
is_default_template = models.BooleanField(default=False,
help_text='If true, this template will be used when creating new containers as default, or preselected')
def __str__(self):
return self.volid.split('/')[-1]
def __repr__(self):
return self.volid
@property
def human_size(self):
return human_size(self.size)

3
proxmox/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
proxmox/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.