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)