361 lines
14 KiB
Python
361 lines
14 KiB
Python
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 save(self, *args, **kwargs):
|
|
if self.is_default_template:
|
|
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)
|
|
|
|
def __str__(self):
|
|
return self.volid.split('/')[-1]
|
|
|
|
def __repr__(self):
|
|
return self.volid
|
|
|
|
@property
|
|
def human_size(self):
|
|
return human_size(self.size)
|
|
|