Commit 6f83043c31cde701499cc0af78a881d458a94f88

Authored by Alexis Koralewski
1 parent e2712b10
Exists in dev

Add websocket for agent_cmd & agent_survey models & views

  1 +13-01-2023 (AKo): v0.6.16.0
  2 + - Add websocket for agent_cmd & agent_survey models & views
  3 +
1 03-01-2023 (AKo): v0.6.15.4 4 03-01-2023 (AKo): v0.6.15.4
2 - new usage of delimiter in cmd parsing in agent_detail view 5 - new usage of delimiter in cmd parsing in agent_detail view
3 6
1 -0.6.15.4  
2 \ No newline at end of file 1 \ No newline at end of file
  2 +0.6.16.0
3 \ No newline at end of file 3 \ No newline at end of file
docker/docker-compose.yml
@@ -25,6 +25,16 @@ services: @@ -25,6 +25,16 @@ services:
25 interval: 5s 25 interval: 5s
26 retries: 20 26 retries: 20
27 27
  28 + redis:
  29 + container_name: redis
  30 + image: redis:latest
  31 + restart: always
  32 + ports:
  33 + - "6379:6379"
  34 + networks:
  35 + pyros-network:
  36 + ipv4_address: 172.19.0.5
  37 +
28 # service image of python, that let users to interact with python scripts such as pyros. 38 # service image of python, that let users to interact with python scripts such as pyros.
29 pyros: 39 pyros:
30 # path to the Dockerfile of this image 40 # path to the Dockerfile of this image
@@ -61,6 +71,7 @@ services: @@ -61,6 +71,7 @@ services:
61 # starting db service before install service 71 # starting db service before install service
62 depends_on: 72 depends_on:
63 - db 73 - db
  74 + - redis
64 # create network to allow images to communicate with other images within the same network 75 # create network to allow images to communicate with other images within the same network
65 networks: 76 networks:
66 pyros-network: 77 pyros-network:
@@ -85,6 +96,8 @@ services: @@ -85,6 +96,8 @@ services:
85 ports: 96 ports:
86 - "${PHPMYADMIN_PORT:-8081}:80" 97 - "${PHPMYADMIN_PORT:-8081}:80"
87 98
  99 +
  100 +
88 # declaring volumes 101 # declaring volumes
89 volumes: 102 volumes:
90 db: 103 db:
install/requirements_dev.txt
@@ -22,3 +22,14 @@ GitPython==3.1.24 @@ -22,3 +22,14 @@ GitPython==3.1.24
22 # For working with date 22 # For working with date
23 python-dateutil==2.8.2 23 python-dateutil==2.8.2
24 24
  25 +# For MCO CNES
  26 +
  27 +astropy
  28 +photutils
  29 +scipy
  30 +
  31 +# for websocket
  32 +channels
  33 +daphne
  34 +attrs==22.1.0
  35 +channels_redis==3.4.1
25 \ No newline at end of file 36 \ No newline at end of file
src/core/pyros_django/api/consumers.py 0 → 100644
@@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
  1 +# consumers.py
  2 +from django.contrib.auth.models import User
  3 +from .serializers import AgentCmdSerializer, AgentSurveySerializer
  4 +from common.models import AgentCmd, AgentSurvey
  5 +from asgiref.sync import async_to_sync
  6 +import json
  7 +from channels.db import database_sync_to_async
  8 +from .functions import get_agent_survey_instance, get_list_agent_cmd
  9 +
  10 +from channels.generic.websocket import AsyncWebsocketConsumer
  11 +
  12 +
  13 +class AgentCmdConsumer(AsyncWebsocketConsumer):
  14 + async def connect(self):
  15 + self.agent_name = self.scope["url_route"]["kwargs"]["agent_name"]
  16 + # Join room group
  17 + await self.channel_layer.group_add(f'agentcmd_{self.agent_name}_observers', self.channel_name)
  18 + await self.accept()
  19 + # try:
  20 + # agent_cmds = await database_sync_to_async(get_list_agent_cmd)(self.agent_name)
  21 + # except Exception as e:
  22 + # print(e)
  23 + await self.channel_layer.group_send(
  24 + f'agentcmd_{self.agent_name}_observers', {"type": "send_agentcmd_instance", "data": None,"action":"list"}
  25 + )
  26 +
  27 + def get_agentcmd(self,id):
  28 + return AgentCmdSerializer(AgentCmd.objects.get(id=id)).data
  29 +
  30 + # async def disconnect(self, close_code):
  31 + # # Leave room group
  32 + # await self.channel_layer.group_discard(f'agentcmd_{self.agent_name}_observers', self.channel_name)
  33 + # await self.close(close_code)
  34 +
  35 + # Receive message from WebSocket
  36 + async def receive(self, text_data):
  37 + text_data_json = json.loads(text_data)
  38 + message = text_data_json["message"]
  39 +
  40 + # Send message to room group
  41 + await self.channel_layer.group_send(
  42 + f'agentcmd_{self.agent_name}_observers', {"type": "new_agentcmd_instance", "agent_name": self.agent_name,"action":"create"}
  43 + )
  44 +
  45 + # ON Receive, send data to observers
  46 + async def send_agentcmd_instance(self, event):
  47 + data = event.get("data",None)
  48 + action = event["action"]
  49 + if action == "list":
  50 + data = await database_sync_to_async(get_list_agent_cmd)(self.agent_name)
  51 + elif action == "create" or action == "update":
  52 + id = data
  53 + data = await database_sync_to_async(self.get_agentcmd)(id)
  54 + # Send message to WebSocket
  55 + await self.send(text_data=json.dumps({"data": data,"action":action}))
  56 +
  57 +
  58 +
  59 +class AgentSurveyDetailConsumer(AsyncWebsocketConsumer):
  60 + async def connect(self):
  61 + self.agent_name = self.scope["url_route"]["kwargs"].get("agent_name")
  62 + print(self.agent_name)
  63 + # Join room group
  64 + await self.channel_layer.group_add(f'agentsurvey_{self.agent_name}_observers', self.channel_name)
  65 + await self.accept()
  66 + # try:
  67 + # agent_survey = await database_sync_to_async(self.get_agent_survey_instance)()
  68 + # except Exception as e:
  69 + # print("exception websocket : ",e)
  70 + await self.channel_layer.group_send(
  71 + f'agentsurvey_{self.agent_name}_observers', {"type": "send_agentsurvey_instance", "data": None}
  72 + )
  73 +
  74 + def get_agent_survey_instance(self):
  75 + agent_survey = AgentSurvey.objects.get(name=self.agent_name)
  76 + return AgentSurveySerializer(agent_survey).data
  77 +
  78 + # async def disconnect(self, close_code):
  79 + # # Leave room group
  80 + # await self.channel_layer.group_discard(f'agentsurvey_{self.agent_name}_observers', self.channel_name)
  81 + # await self.close(close_code)
  82 +
  83 + # Receive message from WebSocket
  84 + async def receive(self, text_data):
  85 + text_data_json = json.loads(text_data)
  86 + message = text_data_json["message"]
  87 +
  88 + # Send message to room group
  89 + await self.channel_layer.group_send(
  90 + f'agentsurvey_{self.agent_name}_observers', {"type": "new_agentsurvey_instance", "agent_name": self.agent_name,"action":"create"}
  91 + )
  92 +
  93 + # Receive message from room group
  94 + async def send_agentsurvey_instance(self, event):
  95 + data = await database_sync_to_async(self.get_agent_survey_instance)()
  96 + # Send message to WebSocket
  97 + await self.send(text_data=json.dumps({"data": data}))
  98 +
  99 +
  100 +class AgentSurveyConsumer(AsyncWebsocketConsumer):
  101 + async def connect(self):
  102 + # Join room group
  103 + await self.channel_layer.group_add(f'agentsurvey_observers', self.channel_name)
  104 + await self.accept()
  105 + # try:
  106 + # agent_survey = await database_sync_to_async(get_agent_survey_instance)()
  107 + # except Exception as e:
  108 + # print(e)
  109 + await self.channel_layer.group_send(
  110 + f'agentsurvey_observers', {"type": "send_agentsurvey_instance", "data": None}
  111 + )
  112 +
  113 +
  114 + # async def disconnect(self, close_code):
  115 + # # Leave room group
  116 + # await self.channel_layer.group_discard(f'agentsurvey_observers', self.channel_name)
  117 + # await self.close(close_code)
  118 +
  119 + # Receive message from WebSocket
  120 + async def receive(self, text_data):
  121 + text_data_json = json.loads(text_data)
  122 + message = text_data_json["message"]
  123 +
  124 + # Send message to room group
  125 + await self.channel_layer.group_send(
  126 + f'agentsurvey_observers', {"type": "new_agentsurvey_instance", "agent_name": self.agent_name,"action":"create"}
  127 + )
  128 +
  129 + # Receive message from room group
  130 + async def send_agentsurvey_instance(self, event):
  131 + data = await database_sync_to_async(get_agent_survey_instance)()
  132 + # Send message to WebSocket
  133 + await self.send(text_data=json.dumps({"data": data}))
src/core/pyros_django/api/functions.py 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +from .serializers import AgentCmdSerializer, AgentSurveySerializer
  2 +from common.models import AgentCmd, AgentSurvey
  3 +from datetime import datetime, timezone, timedelta
  4 +
  5 +
  6 +def get_list_agent_cmd(agent_name):
  7 + commands_sent_by_agent = AgentCmd.get_commands_sent_by_agent(agent_name)
  8 + commands_recivied_by_agent = AgentCmd.get_commands_sent_to_agent(agent_name)
  9 + number = 20
  10 + agent_cmds = commands_sent_by_agent | commands_recivied_by_agent
  11 + agent_cmds = agent_cmds.exclude(full_name="get_specific_cmds")
  12 + agent_cmds = agent_cmds.exclude(full_name="get_all_cmds")
  13 + agent_cmds = agent_cmds.order_by("-s_deposit_time")
  14 + agent_cmds = agent_cmds[:number]
  15 + return AgentCmdSerializer(agent_cmds,many=True).data
  16 +
  17 +
  18 +def get_agent_survey_instance():
  19 + agents = AgentSurvey.objects.all()
  20 + datetime_now = datetime.utcnow()
  21 + date_minus_two_days = datetime_now - timedelta(days=2)
  22 + date_minus_two_days = date_minus_two_days.replace(tzinfo=timezone.utc)
  23 + agents = agents.exclude(updated__lt=date_minus_two_days)
  24 + return AgentSurveySerializer(agents,many=True).data
0 \ No newline at end of file 25 \ No newline at end of file
src/core/pyros_django/api/routing.py 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +from django.urls import path, re_path
  2 +from .views import AgentCmdViewSet
  3 +from .consumers import AgentCmdConsumer, AgentSurveyConsumer, AgentSurveyDetailConsumer
  4 +
  5 +
  6 +websocket_urlpatterns = [
  7 + re_path(r"^ws/agent_cmd/(?P<agent_name>\w+)/$", AgentCmdConsumer.as_asgi()),
  8 + re_path(r"^ws/agent_survey/(?P<agent_name>\w+)/$", AgentSurveyDetailConsumer.as_asgi()),
  9 + re_path(r"^ws/agent_survey/", AgentSurveyConsumer.as_asgi()),
  10 +]
0 \ No newline at end of file 11 \ No newline at end of file
src/core/pyros_django/api/views.py
@@ -294,7 +294,7 @@ class AgentCmdViewSet(viewsets.ModelViewSet): @@ -294,7 +294,7 @@ class AgentCmdViewSet(viewsets.ModelViewSet):
294 http_method_names = ["get"] 294 http_method_names = ["get"]
295 def get_queryset(self): 295 def get_queryset(self):
296 agent_name = self.request.query_params.get('agent_name') 296 agent_name = self.request.query_params.get('agent_name')
297 - number = self.request.query_params.get('number') 297 + number = self.request.query_params.get('number',20)
298 if agent_name is None: 298 if agent_name is None:
299 if "agent_name" in self.kwargs: 299 if "agent_name" in self.kwargs:
300 agent_name = self.kwargs["agent_name"] 300 agent_name = self.kwargs["agent_name"]
src/core/pyros_django/common/models.py
@@ -25,6 +25,10 @@ from django.db.models.expressions import F @@ -25,6 +25,10 @@ from django.db.models.expressions import F
25 from django.db.models.query import QuerySet 25 from django.db.models.query import QuerySet
26 from model_utils import Choices 26 from model_utils import Choices
27 from django.utils import timezone 27 from django.utils import timezone
  28 +from asgiref.sync import async_to_sync
  29 +from channels.layers import get_channel_layer
  30 +from django.db.models.signals import post_save
  31 +from django.dispatch import receiver
28 # Project imports 32 # Project imports
29 from user_manager.models import PyrosUser 33 from user_manager.models import PyrosUser
30 # DeviceCommand is used by class Command 34 # DeviceCommand is used by class Command
@@ -1143,6 +1147,74 @@ class AgentCmd(models.Model): @@ -1143,6 +1147,74 @@ class AgentCmd(models.Model):
1143 # Optimization, but does not work, why ?... 1147 # Optimization, but does not work, why ?...
1144 # self.save(update_fields=["state"]) 1148 # self.save(update_fields=["state"])
1145 1149
  1150 + # override save to use websocket
  1151 + # def save(self, *args, **kwargs):
  1152 + # super().save(*args, **kwargs)
  1153 + # try:
  1154 + # if self.pk == None:
  1155 + # action = "create"
  1156 + # else:
  1157 + # action = "update"
  1158 + # agent_survey = AgentSurvey.objects.all().values_list("name",flat=True)
  1159 + # # initialize value, there is always a sender
  1160 + # agent_name = self.sender
  1161 + # if self.sender in agent_survey:
  1162 + # agent_name = self.sender
  1163 + # # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1164 + # if self.recipient in agent_survey:
  1165 + # agent_name = self.recipient
  1166 + # # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1167 + # async_to_sync(get_channel_layer().group_send)(
  1168 + # f'agentcmd_{agent_name}_observers', {"type": f"send_agentcmd_instance","data":self.id,"action":action}
  1169 + # )
  1170 + # except Exception as e:
  1171 + # print(e)
  1172 +
  1173 +# signals decorator to trigger function after Model AgentCmd call save()
  1174 +@receiver(post_save, sender=AgentCmd)
  1175 +def send_agentcmd_to_websocket(sender, instance, created, **kwargs):
  1176 + agent_survey = AgentSurvey.objects.all().values_list("name",flat=True)
  1177 + if instance.sender in agent_survey:
  1178 + agent_name = instance.sender
  1179 + # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1180 + async_to_sync(get_channel_layer().group_send)(
  1181 + f'agentcmd_{agent_name}_observers', {"type": f"send_agentcmd_instance","action":"list"}
  1182 + )
  1183 + elif instance.recipient in agent_survey:
  1184 + agent_name = instance.recipient
  1185 + # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1186 + async_to_sync(get_channel_layer().group_send)(
  1187 + f'agentcmd_{agent_name}_observers', {"type": f"send_agentcmd_instance","action":"list"}
  1188 + )
  1189 + # if created:
  1190 + # action = "create"
  1191 + # else:
  1192 + # action = "update"
  1193 + # agent_survey = AgentSurvey.objects.all().values_list("name",flat=True)
  1194 + # # initialize value, there is always a sender
  1195 + # agent_name = instance.sender
  1196 + # if instance.sender in agent_survey:
  1197 + # agent_name = selinstancef.sender
  1198 + # # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1199 + # if instance.recipient in agent_survey:
  1200 + # agent_name = instance.recipient
  1201 + # # send to agentcmd_observers group a message to execute function new_agentcmd_agent_name_instance
  1202 + # async_to_sync(get_channel_layer().group_send)(
  1203 + # f'agentcmd_{agent_name}_observers', {"type": f"send_agentcmd_instance","data":instance.id,"action":action}
  1204 + # )
  1205 +
  1206 +
  1207 +@receiver(post_save, sender=AgentSurvey)
  1208 +def send_agentsurvey_to_websocket(sender, instance, created, **kwargs):
  1209 + agent_name = instance.name
  1210 + # send to agentsurvey_observers group a message to execute function new_agentcmd_agent_name_instance
  1211 + async_to_sync(get_channel_layer().group_send)(
  1212 + f'agentsurvey_{agent_name}_observers', {"type": f"send_agentsurvey_instance"}
  1213 + )
  1214 + async_to_sync(get_channel_layer().group_send)(
  1215 + f'agentsurvey_observers', {"type": f"send_agentsurvey_instance"}
  1216 + )
  1217 +
1146 1218
1147 # TODO: OLD Config : à virer (mais utilisé dans dashboard/templatetags/tags.py) 1219 # TODO: OLD Config : à virer (mais utilisé dans dashboard/templatetags/tags.py)
1148 class Config(models.Model): 1220 class Config(models.Model):
src/core/pyros_django/dashboard/templates/dashboard/agent_detail.html
@@ -113,7 +113,7 @@ @@ -113,7 +113,7 @@
113 {% endfor %} {% endcomment %} 113 {% endfor %} {% endcomment %}
114 <div id="additional_fields" style="display:inline;"></div> 114 <div id="additional_fields" style="display:inline;"></div>
115 {% comment %} <input type="text" name="cmd_args" id="cmd_args"></input> {% endcomment %} 115 {% comment %} <input type="text" name="cmd_args" id="cmd_args"></input> {% endcomment %}
116 - <input id="send_cmd_btn" type="submit" value="Send command"></input> 116 + <input id="send_cmd_btn" class="btn btn-info" type="submit" value="Send command"></input>
117 </form> 117 </form>
118 </div> 118 </div>
119 {% comment %} {% else %} {% endcomment %} 119 {% comment %} {% else %} {% endcomment %}
@@ -138,8 +138,11 @@ @@ -138,8 +138,11 @@
138 <thead> 138 <thead>
139 <tr> 139 <tr>
140 <th>Agent</th> 140 <th>Agent</th>
  141 + {% if agentsst_computers %}
  142 + <th> Computer </th>
  143 + {% endif %}
141 <th>Mode</th> 144 <th>Mode</th>
142 - <th style="min-width:10vw;">status</th> 145 + <th style="min-width:16vw;">status</th>
143 {% if CAN_SEND_COMMAND %} 146 {% if CAN_SEND_COMMAND %}
144 <th>Actions</th> 147 <th>Actions</th>
145 {% endif %} 148 {% endif %}
@@ -150,6 +153,9 @@ @@ -150,6 +153,9 @@
150 <tr> 153 <tr>
151 <td><a href="{% url 'agent_detail' agent %}"> {{ agent }}</a> </td> 154 <td><a href="{% url 'agent_detail' agent %}"> {{ agent }}</a> </td>
152 {% comment %} <td> {{ status_of_agents|get_item:agent }}</td> {% endcomment %} 155 {% comment %} <td> {{ status_of_agents|get_item:agent }}</td> {% endcomment %}
  156 + {% if agentsst_computers %}
  157 + <td> {{agentsst_computers|get_item:agent}} </td>
  158 + {% endif %}
153 <td class="agent_mode"></td> 159 <td class="agent_mode"></td>
154 <td class="agent_status"></td> 160 <td class="agent_status"></td>
155 {% if CAN_SEND_COMMAND %} 161 {% if CAN_SEND_COMMAND %}
@@ -463,51 +469,47 @@ @@ -463,51 +469,47 @@
463 get_parameters_of_selected_cmd(); 469 get_parameters_of_selected_cmd();
464 }); 470 });
465 471
466 - function get_agents_status(){  
467 - get_agent_status(); 472 + function update_agents_status(){
468 $('.agent_status').each(function(index){ 473 $('.agent_status').each(function(index){
469 let agent_name = document.getElementsByClassName("agent_status")[index].parentElement.children[0].textContent; 474 let agent_name = document.getElementsByClassName("agent_status")[index].parentElement.children[0].textContent;
470 agent_name = agent_name.trim(); 475 agent_name = agent_name.trim();
471 let agent_status = $(this); 476 let agent_status = $(this);
472 let agent_mode = $(this).prev(); 477 let agent_mode = $(this).prev();
473 - fetch("/api/agent_survey/"+agent_name+"?format=json").then(response => response.json()).then(function(data){  
474 - agent_status.text(data.status);  
475 - agent_mode.text(data.mode);  
476 - if (main_agent_status == "EXITING"){  
477 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = true;  
478 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = true;  
479 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = true;  
480 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = true;  
481 - }  
482 - else{  
483 - if (data.status == "EXITING"){  
484 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = false; 478 +
  479 + if (agent_status != null){
  480 +
  481 + var ws = new WebSocket(`ws://${window.location.host}/ws/agent_survey/${agent_name}/`)
  482 + ws.onmessage = function (e) {
  483 + allData = JSON.parse(e.data);
  484 + data = allData.data;
  485 + agent_status.text(data.status);
  486 + agent_mode.text(data.mode);
  487 + if (main_agent_status == "EXITING"){
  488 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = true;
485 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = true; 489 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = true;
486 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = true; 490 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = true;
487 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = true; 491 document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = true;
488 } 492 }
489 else{ 493 else{
490 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = true;  
491 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = false;  
492 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = false;  
493 - document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = false; 494 + if (data.status == "EXITING"){
  495 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = false;
  496 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = true;
  497 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = true;
  498 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = true;
  499 + }
  500 + else{
  501 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#startbtn").disabled = true;
  502 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#stopbtn").disabled = false;
  503 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restartbtn").disabled = false;
  504 + document.getElementsByClassName("agent_status")[index].parentElement.children[3].querySelector("#restarthardbtn").disabled = false;
  505 + }
  506 +
494 } 507 }
495 -  
496 } 508 }
497 - });  
498 - 509 + }
499 }); 510 });
500 } 511 }
501 - get_agents_status();  
502 - setInterval(get_agents_status,4000);  
503 - var main_agent_status;  
504 - var previous_status = "none";  
505 - async function get_agent_status(){  
506 - await fetch("/api/agent_survey/{{agent_name}}?format=json").then(response => response.json()).then(function(data){  
507 - main_agent_status = data.status;  
508 - });  
509 -  
510 - } 512 +
511 function refresh_agent_info(){ 513 function refresh_agent_info(){
512 514
513 fetch("/api/agent_survey/{{agent_name}}?format=json").then(response => response.json()).then(function(data){ 515 fetch("/api/agent_survey/{{agent_name}}?format=json").then(response => response.json()).then(function(data){
@@ -533,46 +535,64 @@ @@ -533,46 +535,64 @@
533 } 535 }
534 function websocket_agentcmd(){ 536 function websocket_agentcmd(){
535 var ws = new WebSocket(`ws://${window.location.host}/ws/agent_cmd/{{agent_name}}/`) 537 var ws = new WebSocket(`ws://${window.location.host}/ws/agent_cmd/{{agent_name}}/`)
536 - //var ws = new WebSocket(`ws://${window.location.host}/ws/test/{{agent_name}}/`)  
537 - /*ws.onopen = function(){  
538 - ws.send(JSON.stringify({  
539 - action: "list",  
540 - request_id: new Date().getTime(),  
541 - }))  
542 - }*/  
543 - /*  
544 - ws.onopen = function(){  
545 - ws.send(JSON.stringify({  
546 - action: "subscribe_to_agent_cmd_activity",  
547 - request_id: new Date().getTime(),  
548 - }))  
549 - }  
550 - ws.onmessage = function(e){  
551 - console.log(e)  
552 - app.commands = e.data  
553 - }*/  
554 - ws.onopen = function (e) {  
555 - ws.send(  
556 - JSON.stringify({  
557 - action: "list",  
558 - request_id: new Date().getTime(),  
559 - })  
560 - );  
561 - console.log(ws)  
562 - };  
563 - 538 + last_id = null;
564 ws.onmessage = function (e) { 539 ws.onmessage = function (e) {
565 allData = JSON.parse(e.data); 540 allData = JSON.parse(e.data);
566 if (allData.action === "list") { 541 if (allData.action === "list") {
567 app.commands = allData.data; 542 app.commands = allData.data;
568 - app.$forceUpdate();  
569 - } else if (allData.action === "create") {  
570 - app.commands.push(allData.data); 543 + }
  544 + /*
  545 + // OLD (tried to optimize)
  546 + else if (allData.action == "create") {
  547 + console.log("last id");
  548 + console.log(last_id)
  549 + console.log("current id");
  550 + console.log(allData.data.id)
  551 + app.commands.unshift(allData.data);
  552 + last_id = allData.data.id
571 } 553 }
  554 + else if (allData.action == "update"){
  555 + if (last_id == allData.data.id){
  556 + console.log("last id");
  557 + console.log(last_id)
  558 + console.log("current id");
  559 + console.log(allData.data.id)
  560 + app.commands.shift();
  561 + app.commands.unshift(allData.data);
  562 + last_id = allData.data.id
  563 + }
  564 + }
  565 + */
  566 + app.$forceUpdate();
572 }; 567 };
573 - ws.onclose = function(e) { 568 + ws.onerror = function(error) {
574 let now = new Date(); 569 let now = new Date();
575 - console.error(now.toLocaleString() + ':' + now.getMilliseconds() + ' – Analysis socket closed with event code = ' + e.code + ' and reason=' + e.reason); 570 + let msg = now.toLocaleString() + ':' + now.getMilliseconds() + ' – WebSocket error: ' + error;
  571 + console.error(msg);
  572 + }
  573 + }
  574 + var previous_status = "none";
  575 + function websocket_agentsurvey(){
  576 + var ws = new WebSocket(`ws://${window.location.host}/ws/agent_survey/{{agent_name}}/`)
  577 + last_id = null;
  578 + ws.onmessage = function (e) {
  579 + allData = JSON.parse(e.data);
  580 + app2.agent = allData.data;
  581 + data = allData.data
  582 + main_agent_status = data.status
  583 + if (data.status == "EXITING"){
  584 + $("#agent_title").text(data.name + " (OFF)").css("color","red")
  585 + }
  586 + else{
  587 + $("#agent_title").text(data.name + " (ON)").css("color","green")
  588 + }
  589 +
  590 + if (previous_status != data.status){
  591 + display_form_cmd(data.status);
  592 + previous_status = data.status;
  593 + }
  594 +
  595 + app2.$forceUpdate();
576 }; 596 };
577 ws.onerror = function(error) { 597 ws.onerror = function(error) {
578 let now = new Date(); 598 let now = new Date();
@@ -597,12 +617,14 @@ @@ -597,12 +617,14 @@
597 }); 617 });
598 $(document).ready(function() { 618 $(document).ready(function() {
599 619
600 - refresh_agent_info(); 620 + //refresh_agent_info();
601 //setInterval(refresh_command_column,5000); 621 //setInterval(refresh_command_column,5000);
602 - setInterval(refresh_agent_info,4000);  
603 - refresh_cmds_table();  
604 - setInterval(refresh_cmds_table,10000);  
605 - //websocket_agentcmd(); 622 + //setInterval(refresh_agent_info,4000);
  623 + //refresh_cmds_table();
  624 + //setInterval(refresh_cmds_table,10000);
  625 + websocket_agentcmd();
  626 + websocket_agentsurvey();
  627 + update_agents_status();
606 $("cmdform").hide(); 628 $("cmdform").hide();
607 $("cmdform_exiting").hide(); 629 $("cmdform_exiting").hide();
608 }); 630 });
src/core/pyros_django/dashboard/templates/dashboard/agents_state.html
@@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
33 <tr> 33 <tr>
34 <th> Agent </th> 34 <th> Agent </th>
35 <th> Mode </th> 35 <th> Mode </th>
36 - <th> Status </th> 36 + <th style="min-width:16vw;"> Status</th>
37 <th> Udpated </th> 37 <th> Udpated </th>
38 <th> Current command </th> 38 <th> Current command </th>
39 <th> Nb restart attempts </th> 39 <th> Nb restart attempts </th>
@@ -95,11 +95,25 @@ @@ -95,11 +95,25 @@
95 app.agents = data; 95 app.agents = data;
96 }); 96 });
97 } 97 }
  98 + function websocket_agentsurvey(){
  99 + var ws = new WebSocket(`ws://${window.location.host}/ws/agent_survey/`)
  100 + ws.onmessage = function (e) {
  101 + allData = JSON.parse(e.data);
  102 + app.agents = allData.data;
  103 +
  104 + app.$forceUpdate();
  105 + };
  106 + ws.onerror = function(error) {
  107 + let now = new Date();
  108 + let msg = now.toLocaleString() + ':' + now.getMilliseconds() + ' – WebSocket error: ' + error;
  109 + console.error(msg);
  110 + }
  111 + }
98 $(document).ready(function() { 112 $(document).ready(function() {
99 -  
100 - refresh_status_table(); 113 + websocket_agentsurvey();
  114 + //refresh_status_table();
101 //setInterval(refresh_command_column,5000); 115 //setInterval(refresh_command_column,5000);
102 - setInterval(refresh_status_table,4000); 116 + //setInterval(refresh_status_table,4000);
103 }); 117 });
104 var app = new Vue({ 118 var app = new Vue({
105 delimiters:["[[","]]"], 119 delimiters:["[[","]]"],
src/core/pyros_django/dashboard/views.py
@@ -218,8 +218,13 @@ def get_last_all_cmds(agent_name): @@ -218,8 +218,13 @@ def get_last_all_cmds(agent_name):
218 else: 218 else:
219 # AgentSST doesn't have do_stop cmd... (for the moment) 219 # AgentSST doesn't have do_stop cmd... (for the moment)
220 last_agent_all_cmds = AgentCmd.send_cmd_from_to("System",agent_name,"get_all_cmds") 220 last_agent_all_cmds = AgentCmd.send_cmd_from_to("System",agent_name,"get_all_cmds")
  221 + max_wait_time = 3
  222 + current_wait_time = 0
221 while not AgentCmd.objects.get(id=last_agent_all_cmds.id).is_executed() and not AgentCmd.objects.get(id=last_agent_all_cmds.id).is_exec_error(): 223 while not AgentCmd.objects.get(id=last_agent_all_cmds.id).is_executed() and not AgentCmd.objects.get(id=last_agent_all_cmds.id).is_exec_error():
222 time.sleep(0.5) 224 time.sleep(0.5)
  225 + current_wait_time+=0.5
  226 + if max_wait_time <= current_wait_time:
  227 + break
223 return AgentCmd.objects.get(id=last_agent_all_cmds.id) 228 return AgentCmd.objects.get(id=last_agent_all_cmds.id)
224 229
225 @login_required 230 @login_required
@@ -309,14 +314,14 @@ def agent_detail(request, agent_name): @@ -309,14 +314,14 @@ def agent_detail(request, agent_name):
309 return JsonResponse({"agent_general_commands":agent_general_commands,"specific_cmd_with_args":specific_cmd_with_args,"unimplemented_command":unimplemented_command,"cmd_with_choices":cmd_with_choices,"cmds_description":cmds_description},safe=False) 314 return JsonResponse({"agent_general_commands":agent_general_commands,"specific_cmd_with_args":specific_cmd_with_args,"unimplemented_command":unimplemented_command,"cmd_with_choices":cmd_with_choices,"cmds_description":cmds_description},safe=False)
310 if request.GET.get("cmd"): 315 if request.GET.get("cmd"):
311 return JsonResponse(None,safe=False) 316 return JsonResponse(None,safe=False)
312 - commands_sent_by_agent = AgentCmd.get_commands_sent_by_agent(agent_name)  
313 - commands_recivied_by_agent = AgentCmd.get_commands_sent_to_agent(agent_name)  
314 - agent_cmds = commands_sent_by_agent | commands_recivied_by_agent  
315 - agent_cmds = agent_cmds.exclude(full_name="get_specific_cmds")  
316 - agent_cmds = agent_cmds.exclude(full_name="get_all_cmds")  
317 - agent_cmds = agent_cmds.order_by("-s_deposit_time")  
318 - paginator = Paginator(agent_cmds, pyros_settings.NB_ELEMENT_PER_PAGE)  
319 - page_number = request.GET.get("page",1) 317 + # commands_sent_by_agent = AgentCmd.get_commands_sent_by_agent(agent_name)
  318 + # commands_recivied_by_agent = AgentCmd.get_commands_sent_to_agent(agent_name)
  319 + # agent_cmds = commands_sent_by_agent | commands_recivied_by_agent
  320 + # agent_cmds = agent_cmds.exclude(full_name="get_specific_cmds")
  321 + # agent_cmds = agent_cmds.exclude(full_name="get_all_cmds")
  322 + # agent_cmds = agent_cmds.order_by("-s_deposit_time")
  323 + # paginator = Paginator(agent_cmds, pyros_settings.NB_ELEMENT_PER_PAGE)
  324 + # page_number = request.GET.get("page",1)
320 config = OBSConfig(os.environ["PATH_TO_OBSCONF_FILE"],os.environ["unit_name"]) 325 config = OBSConfig(os.environ["PATH_TO_OBSCONF_FILE"],os.environ["unit_name"])
321 managed_agents = None 326 managed_agents = None
322 agents_status = None 327 agents_status = None
@@ -349,12 +354,12 @@ def agent_detail(request, agent_name): @@ -349,12 +354,12 @@ def agent_detail(request, agent_name):
349 break 354 break
350 obj, created = Majordome.objects.get_or_create(id=1) 355 obj, created = Majordome.objects.get_or_create(id=1)
351 CAN_SEND_COMMAND = obj.soft_mode == Majordome.MANUAL_MODE 356 CAN_SEND_COMMAND = obj.soft_mode == Majordome.MANUAL_MODE
352 - try:  
353 - commands = paginator.page(page_number)  
354 - except PageNotAnInteger:  
355 - commands = paginator.page(1)  
356 - except EmptyPage:  
357 - commands = paginator.page(paginator.num_pages) 357 + # try:
  358 + # commands = paginator.page(page_number)
  359 + # except PageNotAnInteger:
  360 + # commands = paginator.page(1)
  361 + # except EmptyPage:
  362 + # commands = paginator.page(paginator.num_pages)
358 return render(request, "dashboard/agent_detail.html", locals()) 363 return render(request, "dashboard/agent_detail.html", locals())
359 364
360 @login_required 365 @login_required
src/core/pyros_django/pyros/asgi.py 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +import os
  2 +
  3 +from channels.auth import AuthMiddlewareStack
  4 +from channels.routing import ProtocolTypeRouter, URLRouter
  5 +from channels.security.websocket import AllowedHostsOriginValidator
  6 +from django.core.asgi import get_asgi_application
  7 +# from django.urls import path
  8 +
  9 +
  10 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pyros.settings")
  11 +# Initialize Django ASGI application early to ensure the AppRegistry
  12 +# is populated before importing code that may import ORM models.
  13 +from api.routing import websocket_urlpatterns
  14 +django_asgi_app = get_asgi_application()
  15 +
  16 +application = ProtocolTypeRouter({
  17 + # Django's ASGI application to handle traditional HTTP requests
  18 + "http": django_asgi_app,
  19 +
  20 + "websocket": AllowedHostsOriginValidator(
  21 + AuthMiddlewareStack(
  22 + URLRouter(websocket_urlpatterns)
  23 + )
  24 + ),
  25 +})
src/core/pyros_django/pyros/settings.py
@@ -191,6 +191,10 @@ if not os.path.isfile(PATH_TO_OBSCONF_FILE): @@ -191,6 +191,10 @@ if not os.path.isfile(PATH_TO_OBSCONF_FILE):
191 # Application definition 191 # Application definition
192 192
193 INSTALLED_APPS = [ 193 INSTALLED_APPS = [
  194 + # websocket server (asgi server)
  195 + 'daphne',
  196 + # websocket for django
  197 + 'channels',
194 'django.contrib.admin', 198 'django.contrib.admin',
195 'django.contrib.auth', 199 'django.contrib.auth',
196 'django.contrib.contenttypes', 200 'django.contrib.contenttypes',
@@ -230,6 +234,17 @@ INSTALLED_APPS = [ @@ -230,6 +234,17 @@ INSTALLED_APPS = [
230 #'kombu.transport.django' 234 #'kombu.transport.django'
231 ] 235 ]
232 236
  237 +CHANNEL_LAYERS = {
  238 + "default": {
  239 +
  240 + "BACKEND": "channels_redis.core.RedisChannelLayer",
  241 + "CONFIG": {
  242 + #"hosts": [("127.0.0.1", 6379)],
  243 + "hosts": [("172.19.0.5", 6379)],
  244 + },
  245 + },
  246 +}
  247 +
233 REST_FRAMEWORK = { 248 REST_FRAMEWORK = {
234 'DEFAULT_AUTHENTICATION_CLASSES': [ 249 'DEFAULT_AUTHENTICATION_CLASSES': [
235 'rest_framework.authentication.TokenAuthentication', 250 'rest_framework.authentication.TokenAuthentication',
@@ -270,6 +285,7 @@ TEMPLATES = [ @@ -270,6 +285,7 @@ TEMPLATES = [
270 ] 285 ]
271 286
272 WSGI_APPLICATION = 'pyros.wsgi.application' 287 WSGI_APPLICATION = 'pyros.wsgi.application'
  288 +ASGI_APPLICATION = "pyros.asgi.application"
273 289
274 FIXTURE_DIRS = ( 290 FIXTURE_DIRS = (
275 'misc/fixtures/', 291 'misc/fixtures/',
@@ -294,7 +310,7 @@ so Django highly recommends activating a strict mode for MySQL to prevent data l @@ -294,7 +310,7 @@ so Django highly recommends activating a strict mode for MySQL to prevent data l
294 mysql_options = { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" } 310 mysql_options = { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" }
295 from src.core.pyros_django.obsconfig.obsconfig_class import OBSConfig 311 from src.core.pyros_django.obsconfig.obsconfig_class import OBSConfig
296 import socket 312 import socket
297 -obsconfig = OBSConfig(os.environ["PATH_TO_OBSCONF_FILE"]) 313 +obsconfig = OBSConfig(os.environ.get("PATH_TO_OBSCONF_FILE",os.path.join(BASE_DIR,"../../../privatedev/config/default/observatory_default.yml")))
298 database_computer = obsconfig.get_database_for_unit(obsconfig.unit_name)["computer"] 314 database_computer = obsconfig.get_database_for_unit(obsconfig.unit_name)["computer"]
299 database_file = obsconfig.get_database_environment(obsconfig.unit_name) 315 database_file = obsconfig.get_database_environment(obsconfig.unit_name)
300 current_computer = socket.gethostname() 316 current_computer = socket.gethostname()
@@ -424,6 +440,23 @@ STATICFILES_DIRS = ( @@ -424,6 +440,23 @@ STATICFILES_DIRS = (
424 # Used for deployment (DEBUG = False). Need to run "python manage.py collectstatic" to fill it. 440 # Used for deployment (DEBUG = False). Need to run "python manage.py collectstatic" to fill it.
425 STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'public', 'static') 441 STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'public', 'static')
426 442
  443 +# LOGGING = {
  444 +# 'version': 1,
  445 +# 'disable_existing_loggers': False,
  446 +# 'handlers': {
  447 +# 'file': {
  448 +# 'level': 'DEBUG',
  449 +# 'class': 'logging.FileHandler',
  450 +# 'filename': '../../../djangodebug.log',
  451 +# },
  452 +# },
  453 +# 'loggers': {
  454 +# 'daphne': {
  455 +# 'handlers': ['file'],
  456 +# 'level': 'DEBUG',
  457 +# },
  458 +# },
  459 +# }
427 460
428 # EP added 461 # EP added
429 if not DEBUG: 462 if not DEBUG: