initial
This commit is contained in:
354
proxmox/models.py
Normal file
354
proxmox/models.py
Normal 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)
|
||||
Reference in New Issue
Block a user