<gh_stars>0
# -*- coding: utf-8 -*-
"""Models for devices, heartbeats, crashreports and log files."""
import logging
import os
import uuid

from django.db import models, transaction, IntegrityError
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.forms import model_to_dict
from taggit.managers import TaggableManager

LOGGER = logging.getLogger(__name__)


class Device(models.Model):
    """A device representing a phone that has been registered on Hiccup."""

    def __str__(self):
        """Return the UUID as string representation of a device."""
        return self.uuid

    # for every device there is a django user
    uuid = models.CharField(
        db_index=True,
        max_length=64,
        unique=True,
        default=uuid.uuid4,
        editable=False,
    )
    user = models.OneToOneField(
        User,
        related_name="Hiccup_Device",
        on_delete=models.CASCADE,
        unique=True,
    )
    imei = models.CharField(max_length=32, null=True, blank=True)
    board_date = models.DateTimeField(null=True, blank=True)
    chipset = models.CharField(max_length=200, null=True, blank=True)
    tags = TaggableManager(blank=True)
    last_heartbeat = models.DateTimeField(null=True, blank=True)
    token = models.CharField(max_length=200, null=True, blank=True)
    next_per_crashreport_key = models.PositiveIntegerField(default=1)
    next_per_heartbeat_key = models.PositiveIntegerField(default=1)

    @transaction.atomic
    def get_crashreport_key(self):
        """Get the next key for a crashreport and update the ID-counter."""
        device = Device.objects.select_for_update().get(id=self.id)
        ret = device.next_per_crashreport_key
        device.next_per_crashreport_key += 1
        device.save()
        return ret

    @transaction.atomic
    def get_heartbeat_key(self):
        """Get the next key for a heartbeat and update the ID-counter."""
        device = Device.objects.select_for_update().get(id=self.id)
        ret = device.next_per_heartbeat_key
        device.next_per_heartbeat_key += 1
        device.save()
        return ret


def crashreport_file_name(instance, filename):
    """Generate the full path for new uploaded log files.

    The path is created by splitting up the device UUID into 3 parts: The
    first 2 characters, the second 2 characters and the rest. This way the
    number of directories in each subdirectory does not get too big.

    Args:
        instance: The log file instance.
        filename: The name of the actual log file.

    Returns: The generated path including the file name.

    """
    return os.path.join(
        str(instance.crashreport.device.uuid)[0:2],
        str(instance.crashreport.device.uuid)[2:4],
        str(instance.crashreport.device.uuid)[4:],
        str(instance.crashreport.id),
        filename,
    )


class Crashreport(models.Model):
    """A crashreport that was sent by a device."""

    BOOT_REASON_UNKOWN = "UNKNOWN"
    BOOT_REASON_KEYBOARD_POWER_ON = "keyboard power on"
    BOOT_REASON_RTC_ALARM = "RTC alarm"
    CRASH_BOOT_REASONS = [BOOT_REASON_UNKOWN, BOOT_REASON_KEYBOARD_POWER_ON]
    SMPL_BOOT_REASONS = [BOOT_REASON_RTC_ALARM]

    device = models.ForeignKey(
        Device,
        db_index=True,
        related_name="crashreports",
        on_delete=models.CASCADE,
    )
    is_fake_report = models.BooleanField(default=False)
    app_version = models.IntegerField()
    uptime = models.CharField(max_length=200)
    build_fingerprint = models.CharField(db_index=True, max_length=200)
    radio_version = models.CharField(db_index=True, max_length=200, null=True)
    boot_reason = models.CharField(db_index=True, max_length=200)
    power_on_reason = models.CharField(db_index=True, max_length=200)
    power_off_reason = models.CharField(db_index=True, max_length=200)
    date = models.DateTimeField(db_index=True)
    tags = TaggableManager(blank=True)
    device_local_id = models.PositiveIntegerField(blank=True)
    next_logfile_key = models.PositiveIntegerField(default=1)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:  # noqa: D106
        unique_together = ("device", "date")

    @transaction.atomic
    def get_logfile_key(self):
        """Get the next key for a log file and update the ID-counter."""
        crashreport = Crashreport.objects.select_for_update().get(id=self.id)
        ret = crashreport.next_logfile_key
        crashreport.next_logfile_key += 1
        crashreport.save()
        return ret

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the crashreport and set its local ID if it was not set."""
        try:
            with transaction.atomic():
                if not self.device_local_id:
                    self.device_local_id = self.device.get_crashreport_key()
                super(Crashreport, self).save(
                    force_insert, force_update, using, update_fields
                )
        except IntegrityError:
            # If there is a duplicate entry, log its values and return
            # without throwing an exception to keep idempotency of the
            # interface.
            LOGGER.debug(
                "Duplicate Crashreport received and dropped: %s",
                model_to_dict(self),
            )

    def _get_uuid(self):
        """Return the device UUID."""
        return self.device.uuid

    uuid = property(_get_uuid)


class LogFile(models.Model):
    """A log file that was sent along with a crashreport."""

    logfile_type = models.TextField(max_length=36, default="last_kmsg")
    crashreport = models.ForeignKey(
        Crashreport, related_name="logfiles", on_delete=models.CASCADE
    )
    logfile = models.FileField(upload_to=crashreport_file_name, max_length=500)
    crashreport_local_id = models.PositiveIntegerField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the log file and set its local ID if it was not set."""
        if not self.crashreport_local_id:
            self.crashreport_local_id = self.crashreport.get_logfile_key()
        super(LogFile, self).save(
            force_insert, force_update, using, update_fields
        )


@receiver(models.signals.post_delete, sender=LogFile)
def auto_delete_file_on_delete(sender, instance, **kwargs):
    """Delete the file from the filesystem on deletion of the db instance."""
    # pylint: disable=unused-argument

    if instance.logfile:
        if os.path.isfile(instance.logfile.path):
            instance.logfile.delete(save=False)


class HeartBeat(models.Model):
    """A heartbeat that was sent by a device."""

    device = models.ForeignKey(
        Device,
        db_index=True,
        related_name="heartbeats",
        on_delete=models.CASCADE,
    )
    app_version = models.IntegerField()
    uptime = models.CharField(max_length=200)
    build_fingerprint = models.CharField(db_index=True, max_length=200)
    radio_version = models.CharField(db_index=True, max_length=200, null=True)
    date = models.DateField(db_index=True)
    device_local_id = models.PositiveIntegerField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:  # noqa: D106
        unique_together = ("device", "date")

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the heartbeat and set its local ID if it was not set."""
        try:
            with transaction.atomic():
                if not self.device_local_id:
                    self.device_local_id = self.device.get_heartbeat_key()
                super(HeartBeat, self).save(
                    force_insert, force_update, using, update_fields
                )
        except IntegrityError:
            # If there is a duplicate entry, log its values and return
            # without throwing an exception to keep idempotency of the
            # interface.
            LOGGER.debug(
                "Duplicate HeartBeat received and dropped: %s",
                model_to_dict(self),
            )

    def _get_uuid(self):
        """Return the device UUID."""
        return self.device.uuid

    uuid = property(_get_uuid)

# -*- coding: utf-8 -*-
"""Models for devices, heartbeats, crashreports and log files."""
import logging
import os
import uuid

from django.db import models, transaction, IntegrityError
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.forms import model_to_dict
from taggit.managers import TaggableManager

LOGGER = logging.getLogger(__name__)


class Device(models.Model):
    """A device representing a phone that has been registered on Hiccup."""

    def __str__(self):
        """Return the UUID as string representation of a device."""
        return self.uuid

    # for every device there is a django user
    uuid = models.CharField(
        db_index=True,
        max_length=64,
        unique=True,
        default=uuid.uuid4,
        editable=False,
    )
    user = models.OneToOneField(
        User,
        related_name="Hiccup_Device",
        on_delete=models.CASCADE,
        unique=True,
    )
    imei = models.CharField(max_length=32, null=True, blank=True)
    board_date = models.DateTimeField(null=True, blank=True)
    chipset = models.CharField(max_length=200, null=True, blank=True)
    tags = TaggableManager(blank=True)
    last_heartbeat = models.DateTimeField(null=True, blank=True)
    token = models.CharField(max_length=200, null=True, blank=True)
    next_per_crashreport_key = models.PositiveIntegerField(default=1)
    next_per_heartbeat_key = models.PositiveIntegerField(default=1)

    @transaction.atomic
    def get_crashreport_key(self):
        """Get the next key for a crashreport and update the ID-counter."""
        device = Device.objects.select_for_update().get(id=self.id)
        ret = device.next_per_crashreport_key
        device.next_per_crashreport_key += 1
        device.save()
        return ret

    @transaction.atomic
    def get_heartbeat_key(self):
        """Get the next key for a heartbeat and update the ID-counter."""
        device = Device.objects.select_for_update().get(id=self.id)
        ret = device.next_per_heartbeat_key
        device.next_per_heartbeat_key += 1
        device.save()
        return ret


def crashreport_file_name(instance, filename):
    """Generate the full path for new uploaded log files.

    The path is created by splitting up the device UUID into 3 parts: The
    first 2 characters, the second 2 characters and the rest. This way the
    number of directories in each subdirectory does not get too big.

    Args:
        instance: The log file instance.
        filename: The name of the actual log file.

    Returns: The generated path including the file name.

    """
    return os.path.join(
        str(instance.crashreport.device.uuid)[0:2],
        str(instance.crashreport.device.uuid)[2:4],
        str(instance.crashreport.device.uuid)[4:],
        str(instance.crashreport.id),
        filename,
    )


class Crashreport(models.Model):
    """A crashreport that was sent by a device."""

    BOOT_REASON_UNKOWN = "UNKNOWN"
    BOOT_REASON_KEYBOARD_POWER_ON = "keyboard power on"
    BOOT_REASON_RTC_ALARM = "RTC alarm"
    CRASH_BOOT_REASONS = [BOOT_REASON_UNKOWN, BOOT_REASON_KEYBOARD_POWER_ON]
    SMPL_BOOT_REASONS = [BOOT_REASON_RTC_ALARM]

    device = models.ForeignKey(
        Device,
        db_index=True,
        related_name="crashreports",
        on_delete=models.CASCADE,
    )
    is_fake_report = models.BooleanField(default=False)
    app_version = models.IntegerField()
    uptime = models.CharField(max_length=200)
    build_fingerprint = models.CharField(db_index=True, max_length=200)
    radio_version = models.CharField(db_index=True, max_length=200, null=True)
    boot_reason = models.CharField(db_index=True, max_length=200)
    power_on_reason = models.CharField(db_index=True, max_length=200)
    power_off_reason = models.CharField(db_index=True, max_length=200)
    date = models.DateTimeField(db_index=True)
    tags = TaggableManager(blank=True)
    device_local_id = models.PositiveIntegerField(blank=True)
    next_logfile_key = models.PositiveIntegerField(default=1)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:  # noqa: D106
        unique_together = ("device", "date")

    @transaction.atomic
    def get_logfile_key(self):
        """Get the next key for a log file and update the ID-counter."""
        crashreport = Crashreport.objects.select_for_update().get(id=self.id)
        ret = crashreport.next_logfile_key
        crashreport.next_logfile_key += 1
        crashreport.save()
        return ret

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the crashreport and set its local ID if it was not set."""
        try:
            with transaction.atomic():
                if not self.device_local_id:
                    self.device_local_id = self.device.get_crashreport_key()
                super(Crashreport, self).save(
                    force_insert, force_update, using, update_fields
                )
        except IntegrityError:
            # If there is a duplicate entry, log its values and return
            # without throwing an exception to keep idempotency of the
            # interface.
            LOGGER.debug(
                "Duplicate Crashreport received and dropped: %s",
                model_to_dict(self),
            )

    def _get_uuid(self):
        """Return the device UUID."""
        return self.device.uuid

    uuid = property(_get_uuid)


class LogFile(models.Model):
    """A log file that was sent along with a crashreport."""

    logfile_type = models.TextField(max_length=36, default="last_kmsg")
    crashreport = models.ForeignKey(
        Crashreport, related_name="logfiles", on_delete=models.CASCADE
    )
    logfile = models.FileField(upload_to=crashreport_file_name, max_length=500)
    crashreport_local_id = models.PositiveIntegerField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the log file and set its local ID if it was not set."""
        if not self.crashreport_local_id:
            self.crashreport_local_id = self.crashreport.get_logfile_key()
        super(LogFile, self).save(
            force_insert, force_update, using, update_fields
        )


@receiver(models.signals.post_delete, sender=LogFile)
def auto_delete_file_on_delete(sender, instance, **kwargs):
    """Delete the file from the filesystem on deletion of the db instance."""
    # pylint: disable=unused-argument

    if instance.logfile:
        if os.path.isfile(instance.logfile.path):
            instance.logfile.delete(save=False)


class HeartBeat(models.Model):
    """A heartbeat that was sent by a device."""

    device = models.ForeignKey(
        Device,
        db_index=True,
        related_name="heartbeats",
        on_delete=models.CASCADE,
    )
    app_version = models.IntegerField()
    uptime = models.CharField(max_length=200)
    build_fingerprint = models.CharField(db_index=True, max_length=200)
    radio_version = models.CharField(db_index=True, max_length=200, null=True)
    date = models.DateField(db_index=True)
    device_local_id = models.PositiveIntegerField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:  # noqa: D106
        unique_together = ("device", "date")

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
    ):
        """Save the heartbeat and set its local ID if it was not set."""
        try:
            with transaction.atomic():
                if not self.device_local_id:
                    self.device_local_id = self.device.get_heartbeat_key()
                super(HeartBeat, self).save(
                    force_insert, force_update, using, update_fields
                )
        except IntegrityError:
            # If there is a duplicate entry, log its values and return
            # without throwing an exception to keep idempotency of the
            # interface.
            LOGGER.debug(
                "Duplicate HeartBeat received and dropped: %s",
                model_to_dict(self),
            )

    def _get_uuid(self):
        """Return the device UUID."""
        return self.device.uuid

    uuid = property(_get_uuid)
# -*- coding: utf-8 -*- Models for devices, heartbeats, crashreports and log files. A device representing a phone that has been registered on Hiccup. Return the UUID as string representation of a device. # for every device there is a django user Get the next key for a crashreport and update the ID-counter. Get the next key for a heartbeat and update the ID-counter. Generate the full path for new uploaded log files.      The path is created by splitting up the device UUID into 3 parts: The     first 2 characters, the second 2 characters and the rest. This way the     number of directories in each subdirectory does not get too big.      Args:         instance: The log file instance.         filename: The name of the actual log file.      Returns: The generated path including the file name. A crashreport that was sent by a device. # noqa: D106 Get the next key for a log file and update the ID-counter. Save the crashreport and set its local ID if it was not set. # If there is a duplicate entry, log its values and return # without throwing an exception to keep idempotency of the # interface. Return the device UUID. A log file that was sent along with a crashreport. Save the log file and set its local ID if it was not set. Delete the file from the filesystem on deletion of the db instance. # pylint: disable=unused-argument A heartbeat that was sent by a device. # noqa: D106 Save the heartbeat and set its local ID if it was not set. # If there is a duplicate entry, log its values and return # without throwing an exception to keep idempotency of the # interface. Return the device UUID.