blob: a40562017cacf939fed9fee8ea1d78e2da3b063b [file] [log] [blame]
Dirk Vogtc9e10ab2016-10-12 13:58:15 +02001# -*- coding: utf-8 -*-
Mitja Nikolaus6a679132018-08-30 14:35:29 +02002"""Models for devices, heartbeats, crashreports and log files."""
Mitja Nikolausfd452f82018-11-07 11:53:59 +01003import logging
Mitja Nikolausb6cf6972018-10-04 15:03:31 +02004import os
Mitja Nikolausbcaf5022018-08-30 16:40:38 +02005import uuid
Dirk Vogtc9e10ab2016-10-12 13:58:15 +02006
Mitja Nikolausfd452f82018-11-07 11:53:59 +01007from django.db import models, transaction, IntegrityError
Dirk Vogtf2a33422016-10-11 17:17:26 +02008from django.contrib.auth.models import User
Mitja Nikolauscc90d572018-11-22 16:40:15 +01009from django.dispatch import receiver
Mitja Nikolausfd452f82018-11-07 11:53:59 +010010from django.forms import model_to_dict
Dirk Vogtf2a33422016-10-11 17:17:26 +020011from taggit.managers import TaggableManager
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020012
Mitja Nikolausfd452f82018-11-07 11:53:59 +010013LOGGER = logging.getLogger(__name__)
14
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020015
Dirk Vogtf2a33422016-10-11 17:17:26 +020016class Device(models.Model):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020017 """A device representing a phone that has been registered on Hiccup."""
18
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020019 def __str__(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020020 """Return the UUID as string representation of a device."""
Dirk Vogt83107df2017-05-02 12:04:19 +020021 return self.uuid
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020022
Dirk Vogtf2a33422016-10-11 17:17:26 +020023 # for every device there is a django user
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020024 uuid = models.CharField(
25 db_index=True,
26 max_length=64,
27 unique=True,
28 default=uuid.uuid4,
29 editable=False,
30 )
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020031 user = models.OneToOneField(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020032 User,
33 related_name="Hiccup_Device",
34 on_delete=models.CASCADE,
35 unique=True,
36 )
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020037 imei = models.CharField(max_length=32, null=True, blank=True)
38 board_date = models.DateTimeField(null=True, blank=True)
39 chipset = models.CharField(max_length=200, null=True, blank=True)
40 tags = TaggableManager(blank=True)
Dirk Vogtf2a33422016-10-11 17:17:26 +020041 last_heartbeat = models.DateTimeField(null=True, blank=True)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020042 token = models.CharField(max_length=200, null=True, blank=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +020043 next_per_crashreport_key = models.PositiveIntegerField(default=1)
44 next_per_heartbeat_key = models.PositiveIntegerField(default=1)
45
46 @transaction.atomic
47 def get_crashreport_key(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020048 """Get the next key for a crashreport and update the ID-counter."""
Mitja Nikolaus76284a42018-11-13 17:12:51 +010049 device = Device.objects.select_for_update().get(id=self.id)
50 ret = device.next_per_crashreport_key
51 device.next_per_crashreport_key += 1
52 device.save()
Dirk Vogt67eb1482016-10-13 12:42:56 +020053 return ret
54
55 @transaction.atomic
56 def get_heartbeat_key(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020057 """Get the next key for a heartbeat and update the ID-counter."""
Mitja Nikolaus76284a42018-11-13 17:12:51 +010058 device = Device.objects.select_for_update().get(id=self.id)
59 ret = device.next_per_heartbeat_key
60 device.next_per_heartbeat_key += 1
61 device.save()
Dirk Vogt67eb1482016-10-13 12:42:56 +020062 return ret
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020063
Dirk Vogtf130c752016-08-23 14:45:01 +020064
65def crashreport_file_name(instance, filename):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020066 """Generate the full path for new uploaded log files.
67
Mitja Nikolausb6cf6972018-10-04 15:03:31 +020068 The path is created by splitting up the device UUID into 3 parts: The
69 first 2 characters, the second 2 characters and the rest. This way the
70 number of directories in each subdirectory does not get too big.
71
Mitja Nikolaus6a679132018-08-30 14:35:29 +020072 Args:
73 instance: The log file instance.
74 filename: The name of the actual log file.
75
76 Returns: The generated path including the file name.
77
78 """
Mitja Nikolausb6cf6972018-10-04 15:03:31 +020079 return os.path.join(
Mitja Nikolausb6cf6972018-10-04 15:03:31 +020080 str(instance.crashreport.device.uuid)[0:2],
81 str(instance.crashreport.device.uuid)[2:4],
82 str(instance.crashreport.device.uuid)[4:],
83 str(instance.crashreport.id),
84 filename,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020085 )
Dirk Vogtf130c752016-08-23 14:45:01 +020086
Dirk Vogtc9e10ab2016-10-12 13:58:15 +020087
Dirk Vogtf130c752016-08-23 14:45:01 +020088class Crashreport(models.Model):
Mitja Nikolaus6a679132018-08-30 14:35:29 +020089 """A crashreport that was sent by a device."""
90
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020091 BOOT_REASON_UNKOWN = "UNKNOWN"
92 BOOT_REASON_KEYBOARD_POWER_ON = "keyboard power on"
93 BOOT_REASON_RTC_ALARM = "RTC alarm"
94 CRASH_BOOT_REASONS = [BOOT_REASON_UNKOWN, BOOT_REASON_KEYBOARD_POWER_ON]
95 SMPL_BOOT_REASONS = [BOOT_REASON_RTC_ALARM]
Franz-Xaver Geiger0b3a48e2018-04-16 15:00:14 +020096
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020097 device = models.ForeignKey(
98 Device,
99 db_index=True,
100 related_name="crashreports",
101 on_delete=models.CASCADE,
102 )
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200103 is_fake_report = models.BooleanField(default=False)
104 app_version = models.IntegerField()
105 uptime = models.CharField(max_length=200)
Dirk Vogt83107df2017-05-02 12:04:19 +0200106 build_fingerprint = models.CharField(db_index=True, max_length=200)
Borjan Tchakaloff6f239a62018-02-19 09:05:50 +0100107 radio_version = models.CharField(db_index=True, max_length=200, null=True)
Dirk Vogt83107df2017-05-02 12:04:19 +0200108 boot_reason = models.CharField(db_index=True, max_length=200)
109 power_on_reason = models.CharField(db_index=True, max_length=200)
110 power_off_reason = models.CharField(db_index=True, max_length=200)
111 date = models.DateTimeField(db_index=True)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200112 tags = TaggableManager(blank=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200113 device_local_id = models.PositiveIntegerField(blank=True)
Dirk Vogt36635692016-10-17 12:19:10 +0200114 next_logfile_key = models.PositiveIntegerField(default=1)
Dirk Vogteda80d32016-11-21 11:45:50 +0100115 created_at = models.DateTimeField(auto_now_add=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200116
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100117 class Meta: # noqa: D106
118 unique_together = ("device", "date")
119
Dirk Vogt67eb1482016-10-13 12:42:56 +0200120 @transaction.atomic
121 def get_logfile_key(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200122 """Get the next key for a log file and update the ID-counter."""
Mitja Nikolaus76284a42018-11-13 17:12:51 +0100123 crashreport = Crashreport.objects.select_for_update().get(id=self.id)
124 ret = crashreport.next_logfile_key
125 crashreport.next_logfile_key += 1
126 crashreport.save()
Dirk Vogt67eb1482016-10-13 12:42:56 +0200127 return ret
128
Mitja Nikolaus773d0cd2018-08-31 10:55:43 +0200129 def save(
130 self,
131 force_insert=False,
132 force_update=False,
133 using=None,
134 update_fields=None,
135 ):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200136 """Save the crashreport and set its local ID if it was not set."""
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100137 try:
138 with transaction.atomic():
139 if not self.device_local_id:
140 self.device_local_id = self.device.get_crashreport_key()
141 super(Crashreport, self).save(
142 force_insert, force_update, using, update_fields
143 )
144 except IntegrityError:
145 # If there is a duplicate entry, log its values and return
146 # without throwing an exception to keep idempotency of the
147 # interface.
148 LOGGER.debug(
149 "Duplicate Crashreport received and dropped: %s",
150 model_to_dict(self),
151 )
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200152
Dirk Vogtf2a33422016-10-11 17:17:26 +0200153 def _get_uuid(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200154 """Return the device UUID."""
Dirk Vogtf2a33422016-10-11 17:17:26 +0200155 return self.device.uuid
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200156
Dirk Vogtf2a33422016-10-11 17:17:26 +0200157 uuid = property(_get_uuid)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200158
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200159
Dirk Vogtf2a33422016-10-11 17:17:26 +0200160class LogFile(models.Model):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200161 """A log file that was sent along with a crashreport."""
162
Dirk Vogt7160b5e2016-10-12 17:04:40 +0200163 logfile_type = models.TextField(max_length=36, default="last_kmsg")
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200164 crashreport = models.ForeignKey(
165 Crashreport, related_name="logfiles", on_delete=models.CASCADE
166 )
Dirk Vogteda80d32016-11-21 11:45:50 +0100167 logfile = models.FileField(upload_to=crashreport_file_name, max_length=500)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200168 crashreport_local_id = models.PositiveIntegerField(blank=True)
Dirk Vogteda80d32016-11-21 11:45:50 +0100169 created_at = models.DateTimeField(auto_now_add=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200170
Mitja Nikolaus773d0cd2018-08-31 10:55:43 +0200171 def save(
172 self,
173 force_insert=False,
174 force_update=False,
175 using=None,
176 update_fields=None,
177 ):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200178 """Save the log file and set its local ID if it was not set."""
Dirk Vogt36635692016-10-17 12:19:10 +0200179 if not self.crashreport_local_id:
180 self.crashreport_local_id = self.crashreport.get_logfile_key()
Mitja Nikolaus773d0cd2018-08-31 10:55:43 +0200181 super(LogFile, self).save(
182 force_insert, force_update, using, update_fields
183 )
Dirk Vogtf2a33422016-10-11 17:17:26 +0200184
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200185
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100186@receiver(models.signals.post_delete, sender=LogFile)
187def auto_delete_file_on_delete(sender, instance, **kwargs):
188 """Delete the file from the filesystem on deletion of the db instance."""
189 # pylint: disable=unused-argument
190
191 if instance.logfile:
192 if os.path.isfile(instance.logfile.path):
193 instance.logfile.delete(save=False)
194
195
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200196class HeartBeat(models.Model):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200197 """A heartbeat that was sent by a device."""
198
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200199 device = models.ForeignKey(
200 Device,
Dirk Vogt83107df2017-05-02 12:04:19 +0200201 db_index=True,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200202 related_name="heartbeats",
203 on_delete=models.CASCADE,
204 )
Dirk Vogt1433f7c2016-09-20 15:30:56 +0200205 app_version = models.IntegerField()
Dirk Vogtf130c752016-08-23 14:45:01 +0200206 uptime = models.CharField(max_length=200)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200207 build_fingerprint = models.CharField(db_index=True, max_length=200)
Borjan Tchakaloff6f239a62018-02-19 09:05:50 +0100208 radio_version = models.CharField(db_index=True, max_length=200, null=True)
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100209 date = models.DateField(db_index=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200210 device_local_id = models.PositiveIntegerField(blank=True)
Dirk Vogteda80d32016-11-21 11:45:50 +0100211 created_at = models.DateTimeField(auto_now_add=True)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200212
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100213 class Meta: # noqa: D106
214 unique_together = ("device", "date")
215
Mitja Nikolaus773d0cd2018-08-31 10:55:43 +0200216 def save(
217 self,
218 force_insert=False,
219 force_update=False,
220 using=None,
221 update_fields=None,
222 ):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200223 """Save the heartbeat and set its local ID if it was not set."""
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100224 try:
225 with transaction.atomic():
226 if not self.device_local_id:
227 self.device_local_id = self.device.get_heartbeat_key()
228 super(HeartBeat, self).save(
229 force_insert, force_update, using, update_fields
230 )
231 except IntegrityError:
232 # If there is a duplicate entry, log its values and return
233 # without throwing an exception to keep idempotency of the
234 # interface.
235 LOGGER.debug(
236 "Duplicate HeartBeat received and dropped: %s",
237 model_to_dict(self),
238 )
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200239
240 def _get_uuid(self):
Mitja Nikolaus6a679132018-08-30 14:35:29 +0200241 """Return the device UUID."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200242 return self.device.uuid
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200243
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200244 uuid = property(_get_uuid)