OTA status views etc
This commit is contained in:
parent
5953bc9acc
commit
ade1807abb
10 changed files with 247 additions and 8 deletions
|
@ -0,0 +1,78 @@
|
|||
# Generated by Django 4.2.1 on 2023-11-26 14:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("mesh", "0011_meshnode_address_validate"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="otaupdaterecipient",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("running", "running"),
|
||||
("replaced", "replaced"),
|
||||
("canceled", "canceled"),
|
||||
("failed", "failed"),
|
||||
("success", "success"),
|
||||
],
|
||||
default="running",
|
||||
max_length=10,
|
||||
verbose_name="status",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="nodemessage",
|
||||
name="message_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("NOOP", "noop"),
|
||||
("ECHO_REQUEST", "echo request"),
|
||||
("ECHO_RESPONSE", "echo response"),
|
||||
("MESH_SIGNIN", "mesh signin"),
|
||||
("MESH_LAYER_ANNOUNCE", "mesh layer announce"),
|
||||
("MESH_ADD_DESTINATIONS", "mesh add destinations"),
|
||||
("MESH_REMOVE_DESTINATIONS", "mesh remove destinations"),
|
||||
("MESH_ROUTE_REQUEST", "mesh route request"),
|
||||
("MESH_ROUTE_RESPONSE", "mesh route response"),
|
||||
("MESH_ROUTE_TRACE", "mesh route trace"),
|
||||
("MESH_ROUTING_FAILED", "mesh routing failed"),
|
||||
("CONFIG_DUMP", "dump config"),
|
||||
("CONFIG_HARDWARE", "hardware config"),
|
||||
("CONFIG_BOARD", "board config"),
|
||||
("CONFIG_FIRMWARE", "firmware config"),
|
||||
("CONFIG_UPLINK", "uplink config"),
|
||||
("CONFIG_POSITION", "position config"),
|
||||
("OTA_STATUS", "ota status"),
|
||||
("OTA_REQUEST_STATUS", "ota request status"),
|
||||
("OTA_START", "ota start"),
|
||||
("OTA_URL", "ota url"),
|
||||
("OTA_FRAGMENT", "ota fragment"),
|
||||
("OTA_REQUEST_FRAGMENTS", "ota request fragments"),
|
||||
("OTA_SETTING", "ota setting"),
|
||||
("OTA_APPLY", "ota apply"),
|
||||
("OTA_ABORT", "ota abort"),
|
||||
("LOCATE_REQUEST_RANGE", "locate request range"),
|
||||
("LOCATE_RANGE_RESULTS", "locate range results"),
|
||||
("LOCATE_RAW_FTM_RESULTS", "locate raw ftm results"),
|
||||
("REBOOT", "reboot"),
|
||||
("REPORT_ERROR", "report error"),
|
||||
],
|
||||
db_index=True,
|
||||
max_length=24,
|
||||
verbose_name="message type",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="otaupdaterecipient",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("status", "running")),
|
||||
fields=("node",),
|
||||
name="only_one_active_ota",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -164,9 +164,10 @@ class MeshNodeQuerySet(models.QuerySet):
|
|||
if nodes is None:
|
||||
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
|
||||
try:
|
||||
for ota in OTAUpdateRecipient.objects.order_by('node', '-update__created').filter(
|
||||
for ota in OTAUpdateRecipient.objects.filter(
|
||||
node__in=nodes.keys(),
|
||||
).select_related("update", "update__build").distinct('node'):
|
||||
status=OTARecipientStatus.RUNNING,
|
||||
).select_related("update", "update__build"):
|
||||
# noinspection PyUnresolvedReferences
|
||||
nodes[ota.node_id]._current_ota = ota
|
||||
for node in nodes.values():
|
||||
|
@ -260,7 +261,9 @@ class MeshNode(models.Model):
|
|||
# noinspection PyUnresolvedReferences
|
||||
return self._current_ota
|
||||
except AttributeError:
|
||||
return self.ota_updates.order_by('-update__created').select_related("update", "update__build").first()
|
||||
return self.ota_updates.select_related("update", "update__build").filter(
|
||||
status=OTARecipientStatus.RUNNING
|
||||
).first()
|
||||
|
||||
@cached_property
|
||||
def ranging_beacon(self) -> Optional["RangingBeacon"]:
|
||||
|
@ -477,8 +480,31 @@ class OTAUpdate(models.Model):
|
|||
build = models.ForeignKey(FirmwareBuild, on_delete=models.CASCADE)
|
||||
created = models.DateTimeField(_('creation'), auto_now_add=True)
|
||||
|
||||
@property
|
||||
def grouped_recipients(self):
|
||||
result = {}
|
||||
for recipient in self.recipients.all():
|
||||
result.setdefault(recipient.get_status_display(), []).append(recipient)
|
||||
return result
|
||||
|
||||
|
||||
class OTARecipientStatus(models.TextChoices):
|
||||
RUNNING = "running", _("running")
|
||||
REPLACED = "replaced", _("replaced")
|
||||
CANCELED = "canceled", _("canceled")
|
||||
FAILED = "failed", _("failed")
|
||||
SUCCESS = "success", _("success")
|
||||
|
||||
|
||||
class OTAUpdateRecipient(models.Model):
|
||||
update = models.ForeignKey(OTAUpdate, on_delete=models.CASCADE, related_name='recipients')
|
||||
node = models.ForeignKey(MeshNode, models.PROTECT, related_name='ota_updates',
|
||||
verbose_name=_('node'))
|
||||
status = models.CharField(max_length=10, choices=OTARecipientStatus.choices, default=OTARecipientStatus.RUNNING,
|
||||
verbose_name=_('status'))
|
||||
|
||||
class Meta:
|
||||
constraints = (
|
||||
UniqueConstraint(fields=["node"], condition=Q(status=OTARecipientStatus.RUNNING),
|
||||
name='only_one_active_ota'),
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<a href="{% url 'mesh.nodes' %}">Nodes</a> ·
|
||||
<a href="{% url 'mesh.messages' %}">Messages</a> ·
|
||||
<a href="{% url 'mesh.firmwares' %}">Firmwares</a> ·
|
||||
<a href="{% url 'mesh.ota.list' %}">OTA</a> ·
|
||||
<a href="{% url 'mesh.ranging' %}">Ranging</a> ·
|
||||
<a href="{% url 'mesh.logs' %}">Live logs</a> ·
|
||||
</p>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if node.current_ota %}
|
||||
<a>#{{ node.current_ota.update.pk }} <small>({{ node.current_ota.update.created }})</small></a><br>
|
||||
<a href="{% url 'mesh.ota.detail' pk=node.current_ota.update.pk %}">#{{ node.current_ota.update.pk }} <small>({{ node.current_ota.update.created }})</small></a><br>
|
||||
{% include "mesh/fragment_firmware_cell.html" with firmware_desc=node.current_ota.update.build.firmware_description %}
|
||||
{% else %}
|
||||
-
|
||||
|
|
37
src/c3nav/mesh/templates/mesh/ota_detail.html
Normal file
37
src/c3nav/mesh/templates/mesh/ota_detail.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n mesh_node %}
|
||||
|
||||
{% block heading %}{% trans 'OTA Update' %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
||||
<div class="columns">
|
||||
<div>
|
||||
<h4>{% trans 'Firmware Build' %}</h4>
|
||||
<strong>Firmware:</strong> <a href="{% url "mesh.firmwares.detail" pk=update.build.version.pk %}">
|
||||
{{ update.build.version.project_name }}
|
||||
{{ update.build.version.version }}
|
||||
</a><br>
|
||||
<strong>Build:</strong> <a href="{% url "mesh.firmwares.build.detail" pk=update.build.pk %}">
|
||||
{{ update.build.variant }}
|
||||
</a><br>
|
||||
<strong>Created:</strong> {{ update.created }}
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Status' %}</th>
|
||||
<th>{% trans 'Progress' %}</th>
|
||||
</tr>
|
||||
{% for recipient in update.recipients.all %}
|
||||
<tr>
|
||||
<td>{% mesh_node recipient.node %}</td>
|
||||
<td>{{ recipient.get_status_display}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
56
src/c3nav/mesh/templates/mesh/ota_list.html
Normal file
56
src/c3nav/mesh/templates/mesh/ota_list.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends 'mesh/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}{% if all %}{% trans 'All OTA Updates' %}{% else %}{% trans 'Running OTA Updates' %}{% endif %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
{% if all %}
|
||||
<a class="button" href="{% url "mesh.ota.list" %}">
|
||||
{% trans 'View running OTA updates only' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="button" href="{% url "mesh.ota.list.all" %}">
|
||||
{% trans 'View all OTA updates' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if all %}
|
||||
{% include 'control/fragment_pagination.html' %}
|
||||
{% endif %}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
<th>{% trans 'Created' %}</th>
|
||||
<th>{% trans 'Firmware' %}</th>
|
||||
<th>{% trans 'Recipients' %}</th>
|
||||
</tr>
|
||||
{% for update in updates %}
|
||||
<tr>
|
||||
<td><a href="{% url 'mesh.ota.detail' pk=update.pk %}">{{ update.pk }}</a></td>
|
||||
<td>{{ update.created }}</td>
|
||||
<td>
|
||||
<a href="{% url "mesh.firmwares.detail" pk=update.build.version.pk %}">
|
||||
{{ update.build.version.project_name }}
|
||||
{{ update.build.version.version }}
|
||||
</a><br>
|
||||
Variant: <a href="{% url "mesh.firmwares.build.detail" pk=update.build.pk %}">
|
||||
{{ update.build.variant }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'mesh.ota.detail' pk=update.pk %}">{{ update.recipients.all | length }} recipients</a><br>
|
||||
{% for status, recipients in update.grouped_recipients.items %}
|
||||
{% if forloop.counter0 > 0 %} • {% endif %}
|
||||
{{ status }}: {{ recipients|length }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
|
||||
{% if all %}
|
||||
{% include 'control/fragment_pagination.html' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -2,7 +2,7 @@ from django.urls import path
|
|||
|
||||
from c3nav.mesh.consumers import MeshConsumer, MeshUIConsumer
|
||||
from c3nav.mesh.views.firmware import (FirmwareBuildDetailView, FirmwareDetailView, FirmwaresCurrentListView,
|
||||
FirmwaresListView)
|
||||
FirmwaresListView, OTAListView, OTADetailView)
|
||||
from c3nav.mesh.views.messages import MeshMessageListView, MeshMessageSendingView, MeshMessageSendView
|
||||
from c3nav.mesh.views.misc import MeshLogView, MeshRangingView
|
||||
from c3nav.mesh.views.nodes import NodeDetailView, NodeEditView, NodeListView
|
||||
|
@ -15,6 +15,9 @@ urlpatterns = [
|
|||
path('firmwares/current/', FirmwaresCurrentListView.as_view(), name='mesh.firmwares.current'),
|
||||
path('firmwares/<int:pk>/', FirmwareDetailView.as_view(), name='mesh.firmwares.detail'),
|
||||
path('firmwares/builds/<int:pk>/', FirmwareBuildDetailView.as_view(), name='mesh.firmwares.build.detail'),
|
||||
path('ota/', OTAListView.as_view(), name='mesh.ota.list'),
|
||||
path('ota/all/', OTAListView.as_view(all=True), name='mesh.ota.list.all'),
|
||||
path('ota/<int:pk>/', OTADetailView.as_view(), name='mesh.ota.detail'),
|
||||
path('nodes/<str:pk>/', NodeDetailView.as_view(), name='mesh.node.detail'),
|
||||
path('nodes/<str:pk>/edit/', NodeEditView.as_view(), name='mesh.node.edit'),
|
||||
path('message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='mesh.sending'),
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode
|
||||
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode, OTAUpdate, OTARecipientStatus
|
||||
from c3nav.mesh.views.base import MeshControlMixin
|
||||
from c3nav.site.forms import OTACreateForm
|
||||
|
||||
|
@ -87,3 +87,35 @@ class FirmwareBuildDetailView(OTACreateMixin, MeshControlMixin, DetailView):
|
|||
**super().get_form_kwargs(),
|
||||
'builds': [self.get_object()],
|
||||
}
|
||||
|
||||
|
||||
class OTAListView(ListView):
|
||||
model = OTAUpdate
|
||||
template_name = "mesh/ota_list.html"
|
||||
ordering = "-created"
|
||||
context_object_name = "updates"
|
||||
all = False
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().prefetch_related('recipients')
|
||||
if self.all:
|
||||
qs = qs.filter(recipients__status=OTARecipientStatus.RUNNING)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
**super().get_context_data(),
|
||||
"all": self.all,
|
||||
}
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
return 20 if self.all else None
|
||||
|
||||
|
||||
class OTADetailView(MeshControlMixin, DetailView):
|
||||
model = OTAUpdate
|
||||
template_name = "mesh/ota_detail.html"
|
||||
context_object_name = "update"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related('recipients', 'recipients__node')
|
||||
|
|
|
@ -19,7 +19,6 @@ class NodeListView(MeshControlMixin, ListView):
|
|||
return super().get_queryset().prefetch_last_messages().prefetch_firmwares().prefetch_ranging_beacon()
|
||||
|
||||
|
||||
|
||||
class NodeDetailView(MeshControlMixin, DetailView):
|
||||
model = MeshNode
|
||||
template_name = "mesh/node_detail.html"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import replace as dataclass_replace
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
@ -12,7 +13,8 @@ from c3nav.mapdata.forms import I18nModelFormMixin
|
|||
from c3nav.mapdata.models.locations import Position
|
||||
from c3nav.mapdata.models.report import Report, ReportUpdate
|
||||
from c3nav.mesh.messages import MeshMessageType
|
||||
from c3nav.mesh.models import FirmwareBuild, HardwareDescription, MeshNode, OTAUpdate
|
||||
from c3nav.mesh.models import FirmwareBuild, HardwareDescription, MeshNode, OTAUpdate, OTAUpdateRecipient, \
|
||||
OTARecipientStatus
|
||||
|
||||
|
||||
class ReportIssueForm(I18nModelFormMixin, ModelForm):
|
||||
|
@ -154,6 +156,11 @@ class OTACreateForm(Form):
|
|||
def save(self) -> list[OTAUpdate]:
|
||||
updates = []
|
||||
with transaction.atomic():
|
||||
replaced_recipients = OTAUpdateRecipient.objects.filter(
|
||||
node__in=chain(*self.selected_builds.values()),
|
||||
status=OTARecipientStatus.RUNNING,
|
||||
).select_for_update()
|
||||
replaced_recipients.update(status=OTARecipientStatus.REPLACED)
|
||||
for build, nodes in self.selected_builds.items():
|
||||
update = OTAUpdate.objects.create(build=build)
|
||||
for node in nodes:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue