Files
Django-Proxmox-Mikrotik/proxmox/models.py

376 lines
14 KiB
Python
Raw Normal View History

2025-08-27 09:55:55 +02:00
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, write_to_db=True, **kwargs):
2025-08-27 09:55:55 +02:00
"""Sets the values in DB
Extracts csv Values into self, if given"""
2025-08-27 09:55:55 +02:00
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}")
if write_to_db:
return self.write(**params)
else:
return params
2025-09-04 10:15:41 +02:00
@property
def vmstatus(self):
raise NotImplemented('vmstatus not implemented')
2025-08-27 09:55:55 +02:00
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)),
}
2025-09-04 10:15:41 +02:00
@property
def synced_status(self):
with Proxmox() as pm:
if statusresponse := pm.lxc_get(f'{self.vmid}/status/current'):
self.write(status=statusresponse['status'])
return self.status
2025-08-27 09:55:55 +02:00
@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()
statusdata = pm.lxc_get(f'{self.vmid}/status/current')
data |= statusdata
self.from_proxmox(write_to_db=True, **data)
2025-08-27 09:55:55 +02:00
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 save(self, *args, **kwargs):
if self.is_default_template:
2025-08-27 10:24:07 +02:00
self.__class__.objects.filter(models.Q(is_default_template=True), ~models.Q(internal_id=self.pk)).update(is_default_template=False)
super().save(*args, **kwargs)
2025-08-27 09:55:55 +02:00
def __str__(self):
return self.volid.split('/')[-1]
def __repr__(self):
return self.volid
@property
def human_size(self):
return human_size(self.size)