blob: a40562017cacf939fed9fee8ea1d78e2da3b063b [file] [log] [blame]
# -*- 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)