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:
|
if nodes is None:
|
||||||
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
|
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
|
||||||
try:
|
try:
|
||||||
for ota in OTAUpdateRecipient.objects.order_by('node', '-update__created').filter(
|
for ota in OTAUpdateRecipient.objects.filter(
|
||||||
node__in=nodes.keys(),
|
node__in=nodes.keys(),
|
||||||
).select_related("update", "update__build").distinct('node'):
|
status=OTARecipientStatus.RUNNING,
|
||||||
|
).select_related("update", "update__build"):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
nodes[ota.node_id]._current_ota = ota
|
nodes[ota.node_id]._current_ota = ota
|
||||||
for node in nodes.values():
|
for node in nodes.values():
|
||||||
|
@ -260,7 +261,9 @@ class MeshNode(models.Model):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
return self._current_ota
|
return self._current_ota
|
||||||
except AttributeError:
|
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
|
@cached_property
|
||||||
def ranging_beacon(self) -> Optional["RangingBeacon"]:
|
def ranging_beacon(self) -> Optional["RangingBeacon"]:
|
||||||
|
@ -477,8 +480,31 @@ class OTAUpdate(models.Model):
|
||||||
build = models.ForeignKey(FirmwareBuild, on_delete=models.CASCADE)
|
build = models.ForeignKey(FirmwareBuild, on_delete=models.CASCADE)
|
||||||
created = models.DateTimeField(_('creation'), auto_now_add=True)
|
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):
|
class OTAUpdateRecipient(models.Model):
|
||||||
update = models.ForeignKey(OTAUpdate, on_delete=models.CASCADE, related_name='recipients')
|
update = models.ForeignKey(OTAUpdate, on_delete=models.CASCADE, related_name='recipients')
|
||||||
node = models.ForeignKey(MeshNode, models.PROTECT, related_name='ota_updates',
|
node = models.ForeignKey(MeshNode, models.PROTECT, related_name='ota_updates',
|
||||||
verbose_name=_('node'))
|
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.nodes' %}">Nodes</a> ·
|
||||||
<a href="{% url 'mesh.messages' %}">Messages</a> ·
|
<a href="{% url 'mesh.messages' %}">Messages</a> ·
|
||||||
<a href="{% url 'mesh.firmwares' %}">Firmwares</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.ranging' %}">Ranging</a> ·
|
||||||
<a href="{% url 'mesh.logs' %}">Live logs</a> ·
|
<a href="{% url 'mesh.logs' %}">Live logs</a> ·
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.current_ota %}
|
{% 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 %}
|
{% include "mesh/fragment_firmware_cell.html" with firmware_desc=node.current_ota.update.build.firmware_description %}
|
||||||
{% else %}
|
{% 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.consumers import MeshConsumer, MeshUIConsumer
|
||||||
from c3nav.mesh.views.firmware import (FirmwareBuildDetailView, FirmwareDetailView, FirmwaresCurrentListView,
|
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.messages import MeshMessageListView, MeshMessageSendingView, MeshMessageSendView
|
||||||
from c3nav.mesh.views.misc import MeshLogView, MeshRangingView
|
from c3nav.mesh.views.misc import MeshLogView, MeshRangingView
|
||||||
from c3nav.mesh.views.nodes import NodeDetailView, NodeEditView, NodeListView
|
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/current/', FirmwaresCurrentListView.as_view(), name='mesh.firmwares.current'),
|
||||||
path('firmwares/<int:pk>/', FirmwareDetailView.as_view(), name='mesh.firmwares.detail'),
|
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('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>/', NodeDetailView.as_view(), name='mesh.node.detail'),
|
||||||
path('nodes/<str:pk>/edit/', NodeEditView.as_view(), name='mesh.node.edit'),
|
path('nodes/<str:pk>/edit/', NodeEditView.as_view(), name='mesh.node.edit'),
|
||||||
path('message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='mesh.sending'),
|
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 import DetailView, ListView, TemplateView
|
||||||
from django.views.generic.edit import FormMixin
|
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.mesh.views.base import MeshControlMixin
|
||||||
from c3nav.site.forms import OTACreateForm
|
from c3nav.site.forms import OTACreateForm
|
||||||
|
|
||||||
|
@ -87,3 +87,35 @@ class FirmwareBuildDetailView(OTACreateMixin, MeshControlMixin, DetailView):
|
||||||
**super().get_form_kwargs(),
|
**super().get_form_kwargs(),
|
||||||
'builds': [self.get_object()],
|
'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()
|
return super().get_queryset().prefetch_last_messages().prefetch_firmwares().prefetch_ranging_beacon()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NodeDetailView(MeshControlMixin, DetailView):
|
class NodeDetailView(MeshControlMixin, DetailView):
|
||||||
model = MeshNode
|
model = MeshNode
|
||||||
template_name = "mesh/node_detail.html"
|
template_name = "mesh/node_detail.html"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dataclasses import replace as dataclass_replace
|
from dataclasses import replace as dataclass_replace
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Any, Sequence
|
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.locations import Position
|
||||||
from c3nav.mapdata.models.report import Report, ReportUpdate
|
from c3nav.mapdata.models.report import Report, ReportUpdate
|
||||||
from c3nav.mesh.messages import MeshMessageType
|
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):
|
class ReportIssueForm(I18nModelFormMixin, ModelForm):
|
||||||
|
@ -154,6 +156,11 @@ class OTACreateForm(Form):
|
||||||
def save(self) -> list[OTAUpdate]:
|
def save(self) -> list[OTAUpdate]:
|
||||||
updates = []
|
updates = []
|
||||||
with transaction.atomic():
|
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():
|
for build, nodes in self.selected_builds.items():
|
||||||
update = OTAUpdate.objects.create(build=build)
|
update = OTAUpdate.objects.create(build=build)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue