diff --git a/db.sqlite3 b/db.sqlite3 index 432f204be352251f792e0f91a1b7377303918b43..e33654f60a42bd1bdaf59e30dc8587fc89bd53be 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/iotamak_ihm/settings.py b/iotamak_ihm/settings.py index fd177dfd041688a0df503d5912a7c84bdcd48738..3f506acfe0eb5c66886aaf1ac7acf80690289250 100644 --- a/iotamak_ihm/settings.py +++ b/iotamak_ihm/settings.py @@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ - +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -27,6 +27,7 @@ DEBUG = True ALLOWED_HOSTS = [] +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads/') # Application definition diff --git a/iotamak_ihm/urls.py b/iotamak_ihm/urls.py index d502811807ace9b547f777a215ca3ecc1952087f..a05677f753dfe0e74aab042c04def288101f8d56 100644 --- a/iotamak_ihm/urls.py +++ b/iotamak_ihm/urls.py @@ -17,6 +17,6 @@ from django.contrib import admin from django.urls import path, include urlpatterns = [ - path('ping/', include('ping.urls')), + path('', include('ping.urls')), path('admin/', admin.site.urls), ] diff --git a/ping/admin.py b/ping/admin.py index 7e490c7617f3c6324bf80425169b87d26cbfc5ff..f8dd658921a7e457d2651b11af8b8e020a498a0b 100644 --- a/ping/admin.py +++ b/ping/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin -from .models import Client +from .models import Client, Agent, Experiment -admin.site.register(Client) \ No newline at end of file +admin.site.register(Client) +admin.site.register(Agent) +admin.site.register(Experiment) \ No newline at end of file diff --git a/ping/migrations/0001_initial.py b/ping/migrations/0001_initial.py index 7eda40bfb31a9d3d6c2aa8c66bd1b1c8534e24cf..ae0c37f73e11cbb2e187439556f37a476c144143 100644 --- a/ping/migrations/0001_initial.py +++ b/ping/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.4 on 2022-05-10 15:39 +# Generated by Django 4.0.4 on 2022-05-12 08:50 from django.db import migrations, models @@ -11,12 +11,21 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Agent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip', models.CharField(max_length=16)), + ('command', models.CharField(max_length=256)), + ], + ), migrations.CreateModel( name='Client', fields=[ ('hostname', models.CharField(max_length=16, primary_key=True, serialize=False, unique=True)), ('username', models.CharField(max_length=16)), ('password', models.CharField(max_length=16)), + ('status', models.CharField(default='Offline', max_length=8)), ], ), ] diff --git a/ping/migrations/0002_experiment.py b/ping/migrations/0002_experiment.py new file mode 100644 index 0000000000000000000000000000000000000000..c06c6d60dcf15c768fd20542a0605ce4ebb46062 --- /dev/null +++ b/ping/migrations/0002_experiment.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.4 on 2022-05-12 12:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ping', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Experiment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=60)), + ('status', models.CharField(default='Not checked', max_length=60)), + ('description', models.TextField()), + ('media', models.FileField(blank=True, null=True, upload_to='')), + ], + ), + ] diff --git a/ping/models.py b/ping/models.py index 7a1725a436281e8ce65e2c1f6fde8620e3a9a2f5..c6a47a184abdd614fc87b20e17fb3f7a3a709b3a 100644 --- a/ping/models.py +++ b/ping/models.py @@ -1,10 +1,32 @@ from django.db import models +from django.forms import ModelForm class Client(models.Model): hostname = models.CharField(max_length=16, primary_key=True, unique=True) username = models.CharField(max_length=16) password = models.CharField(max_length=16) + status = models.CharField(max_length=8, default="Offline") def __str__(self): - return "Hostname : " + self.hostname + " Username : " + self.username \ No newline at end of file + return "Hostname : " + self.hostname + " Username : " + self.username + " Status : " + self.status + + +class Agent(models.Model): + ip = models.CharField(max_length=16) + command = models.CharField(max_length=256) + + def __str__(self): + return self.command + +class Experiment(models.Model): + name = models.CharField(max_length=60) + status = models.CharField(max_length=60, default="Not checked") + description = models.TextField() + media = models.FileField(upload_to="media", null=True, blank=True) + + +class ExperimentForm(ModelForm): + class Meta: + model = Experiment + fields = ["name", "description", "media"] diff --git a/ping/static/ping/index.css b/ping/static/ping/index.css new file mode 100644 index 0000000000000000000000000000000000000000..3e4d20680a3dccf9b8ce06c5e1e8ef0aa70b7d82 --- /dev/null +++ b/ping/static/ping/index.css @@ -0,0 +1,41 @@ +body { + margin: 0px; + font-family: Arial, Helvetica, sans-serif; +} + +.topnav { + overflow: hidden; + background-color: #333; +} + +.topnav a { + float: left; + color: #f2f2f2; + text-align: center; + padding: 14px 16px; + text-decoration: none; + font-size: 17px; +} + +.topnav a:hover { + background-color: #ddd; + color: black; +} + +.topnav a.active { + background-color: #0486aa; + color: white; +} + +th { + text-align: center; +} +tr:nth-child(even) {background-color: #f2f2f2;} +td { + height: 25px; + vertical-align: bottom; +} + +th, td { + border-bottom: thin solid #ddd; +} \ No newline at end of file diff --git a/ping/templates/ping/entry.html b/ping/templates/ping/entry.html new file mode 100644 index 0000000000000000000000000000000000000000..b718dd690422ca999abb822edda5e42a6a8dd29b --- /dev/null +++ b/ping/templates/ping/entry.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + {% load static %} + <link rel="stylesheet" type="text/css" href="{% static 'ping/index.css' %}"> + + <title>Experiment - IOTAMAK</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + +</head> + +<body> + + <div class="topnav"> + <a href="/network">Network</a> + <a class="active" href="/experiment">Experiment</a> + </div> + <form action="/experiment/new/" method="POST" enctype="multipart/form-data"> + {% csrf_token %} + {% for entry in form %} + <div> + {{ entry.label_tag }} + </div> + <div> + {{entry}} + </div> + {% endfor %} + <button> + Save! + </button> + </form> +</body> +</html> \ No newline at end of file diff --git a/ping/templates/ping/experiment.html b/ping/templates/ping/experiment.html new file mode 100644 index 0000000000000000000000000000000000000000..86454db688d053b9a18b89d3b7fb044bf907711c --- /dev/null +++ b/ping/templates/ping/experiment.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + {% load static %} + <link rel="stylesheet" type="text/css" href="{% static 'ping/index.css' %}"> + + <title>Experiment - IOTAMAK</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + +</head> + +<body> + + <div class="topnav"> + <a href="/network">Network</a> + <a class="active" href="/experiment">Experiment</a> + </div> + + <form action='new' method='GET'> + <button type='submit'> Add experiment</button> + </form> + + {% if experiments %} + <table> + <tr> + <th>Name</th> + <th>Status</th> + <th>Check</th> + </tr> + {% for experiment in experiments %} + <tr> + <td>{{ experiment.name }}</td> + <td>{{ experiment.status }}</td> + <td><a href="/experiment/{{ experiment.id }}/check">Check</td> + </tr> + {% endfor %} + </table> + {% else %} + <p>No agents are available.</p> + {% endif %} + +</body> + +</html> \ No newline at end of file diff --git a/ping/templates/ping/index.html b/ping/templates/ping/index.html index 02097b415786bc2d0557e02fda3857d2b975db4e..e39a748b2828a0220baaa7799f4de10fdaef5575 100644 --- a/ping/templates/ping/index.html +++ b/ping/templates/ping/index.html @@ -1,19 +1,68 @@ <!DOCTYPE html> <html> - <head> - <meta charset="utf-8"> - <title>Main menu</title> - </head> - <body> - <form action='pressed' method='GET'> - <button type='submit'> Ping clients</button> - </form> - - <ul> - {% for client in host_list %} - <li>{{client}}</li> - {% endfor %} - </ul> - - </body> + +<head> + <meta charset="utf-8"> + {% load static %} + <link rel="stylesheet" type="text/css" href="{% static 'ping/index.css' %}"> + + <title>Network - IOTAMAK</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + +</head> + +<body> + <div class="topnav"> + <a class="active" href="/network">Network</a> + <a href="/experiment">Experiment</a> + </div> + + + + <form action='pressed' method='GET'> + <button type='submit'> Ping clients</button> + </form> + + <form action='update' method='GET'> + <button type='submit'> Update clients</button> + </form> + + <form action='agents' method='GET'> + <button type='submit'> agents</button> + </form> + + <table> + <tr> + <th>Hostname</th> + <th>Username</th> + <th>Status</th> + </tr> + {% for client in host_list %} + <tr> + <td>{{ client.hostname }}</td> + <td>{{ client.username }}</td> + <td>{{ client.status }}</td> + </tr> + {% endfor %} + </table> + + {% if agents %} + <table> + <tr> + <th>Hostname</th> + <th>Agent</th> + </tr> + {% for agent in agents %} + <tr> + <td>{{ agent.ip }}</td> + <td>{{ agent.command }}</td> + </tr> + {% endfor %} + </table> + {% else %} + <p>No agents are available.</p> + {% endif %} + +</body> + </html> \ No newline at end of file diff --git a/ping/urls.py b/ping/urls.py index de7ace3d352acd09e9d9be367e27a2af29bf9b01..f7f30028c5c1823c88de037886b4b053f0628569 100644 --- a/ping/urls.py +++ b/ping/urls.py @@ -4,6 +4,11 @@ from . import views app_name = 'ping' urlpatterns = [ - path('', views.index, name='index'), - path('pressed/', views.pressed, name='pressed'), + path('network/', views.index, name='index'), + path('network/pressed/', views.pressed, name='pressed'), + path('network/update/', views.update, name='update'), + path('network/agents/', views.agents, name='agents'), + path('experiment/new/', views.entry, name='entry'), + path('experiment/', views.experiment, name='experiment'), + path('experiment/<int:experiment_id>/check/', views.check, name='check'), ] \ No newline at end of file diff --git a/ping/views.py b/ping/views.py index 948a0fa55bd848975c98721aee5172c9b1490740..9cd930554ebde50d240e380e6008034f0c3391e4 100644 --- a/ping/views.py +++ b/ping/views.py @@ -1,21 +1,97 @@ +import os import platform +import shutil import subprocess +import re +import zipfile + +from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from django.template import loader from django.urls import reverse +from iotAmak.tool.remote_client import RemoteClient +from iotAmak.tool.ssh_client import SSHClient, Cmd + +from .models import Client, Agent, Experiment, ExperimentForm + + +def get_remote_client(): + res = [] + for client in Client.objects.all(): + if client.status == "Online": + res.append(RemoteClient(client.hostname, client.username, client.password)) + + return res + + +def get_ssh_client(): + return SSHClient(get_remote_client()) + -from .models import Client +def update(request): + ssh = get_ssh_client() + version = "0.0.1" + commands = [ + Cmd( + cmd="cd Desktop/mqtt_goyon/iotamak-core" + ), + Cmd( + cmd="git pull" + ), + Cmd( + cmd="git checkout main" + ), + Cmd( + cmd="git pull" + ), + Cmd( + cmd="python3 -m pip install --force-reinstall dist/iotAmak-" + version + "-py3-none-any.whl" + ), + Cmd( + cmd="cd ../../../" + ) # , + # Cmd( + # cmd="rm -r Desktop/mqtt_goyon/example/" + self.experiment_name + # ) + + ] + for i_client in range(len(ssh.clients)): + print("Hostname :", ssh.clients[i_client].hostname, " User :", ssh.clients[i_client].user) + ssh.run_cmd(i_client, commands) + + return HttpResponseRedirect(reverse('ping:index')) + + +def agents(request): + ssh = get_ssh_client() + Agent.objects.all().delete() + commands = [ + Cmd( + cmd="ps -ef | tr -s ' ' | cut -d ' ' -f 8-", + do_print=False + )] + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + + for i_client in range(len(ssh.clients)): + raw_string = ssh.run_cmd(i_client, commands)[0].split("\r\n") + raw_string = [i for i in raw_string if "python D" in i] + for line in raw_string: + new_entry = Agent(ip=ssh.clients[i_client].hostname, command=ansi_escape.sub('', line)) + new_entry.save() + + return HttpResponseRedirect(reverse('ping:index')) def index(request): template = loader.get_template('ping/index.html') - host_list = Client.objects.all() context = { - "host_list": host_list, + "host_list": Client.objects.all(), + "agents": Agent.objects.all() } return HttpResponse(template.render(context, request)) + def pressed(request): for client in Client.objects.all(): param = '-n' if platform.system().lower() == 'windows' else '-c' @@ -23,6 +99,84 @@ def pressed(request): response = subprocess.call(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 - print(response) + c = Client.objects.get(hostname=client.hostname) + if response: + c.status = "Online" + else: + c.status = "Offline" + c.save() + + return HttpResponseRedirect(reverse('ping:index')) - return HttpResponseRedirect(reverse('ping:index')) \ No newline at end of file + +def entry(request): + if request.method == 'POST': + form = ExperimentForm(request.POST, request.FILES) + + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('ping:experiment')) + + else: + form = ExperimentForm() + template = loader.get_template('ping/entry.html') + context = {"form": form} + return HttpResponse(template.render(context, request)) + +def empty_tmp(): + folder = str(settings.MEDIA_ROOT) + "/tmp" + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + print('Failed to delete %s. Reason: %s' % (file_path, e)) + +def check(request, experiment_id): + # get the path + print(experiment_id) + exp = Experiment.objects.get(pk=experiment_id) + print(str(exp.media)) + print(str(settings.MEDIA_ROOT) + str(exp.media)) + # check if it's a zip -> wrong format : not zip + print() + + if not zipfile.is_zipfile(str(settings.MEDIA_ROOT) + str(exp.media)): + exp.status = "Wrong format : zip file expected" + exp.save() + return HttpResponseRedirect(reverse('ping:experiment')) + + with zipfile.ZipFile(str(settings.MEDIA_ROOT) + str(exp.media), 'r') as zip_ref: + zip_ref.extractall(str(settings.MEDIA_ROOT) + "/tmp") + + folder_path = str(settings.MEDIA_ROOT) + "/tmp/" + exp.name + + if not os.path.isdir(folder_path): + exp.status = "Wrong format : zip should contain a folder" + exp.save() + empty_tmp() + return HttpResponseRedirect(reverse('ping:experiment')) + + required_files = ["amas.py", "agent.py", "env.py", "scheduler.py"] + for required_file in required_files: + if not os.path.exists(folder_path + "/" + required_file): + exp.status = "Wrong format : " + required_file + " file is expected" + exp.save() + empty_tmp() + return HttpResponseRedirect(reverse('ping:experiment')) + + exp.status = "Checked" + exp.save() + empty_tmp() + return HttpResponseRedirect(reverse('ping:experiment')) + + +def experiment(request): + template = loader.get_template('ping/experiment.html') + context = { + "experiments": Experiment.objects.all(), + } + return HttpResponse(template.render(context, request)) diff --git a/uploads/media/config.json b/uploads/media/config.json new file mode 100644 index 0000000000000000000000000000000000000000..56c5b0c82bcd9281d321f063c9d5bcd5237c6b1f --- /dev/null +++ b/uploads/media/config.json @@ -0,0 +1,25 @@ +{ + "broker" : "192.168.30.209", + "clients_ssh" : [ + { + "hostname" : "192.168.30.18", + "user" : "pi", + "password" : "raspberry" + }, + { + "hostname" : "192.168.30.227", + "user" : "pi", + "password" : "raspberry" + }, + { + "hostname" : "192.168.30.61", + "user" : "pi", + "password" : "raspberry" + }, + { + "hostname" : "192.168.30.75", + "user" : "pi", + "password" : "raspberry" + } + ] +} \ No newline at end of file diff --git a/uploads/media/env.py b/uploads/media/env.py new file mode 100644 index 0000000000000000000000000000000000000000..c8a767fd18a98aabce6994c1864789c28a374ee5 --- /dev/null +++ b/uploads/media/env.py @@ -0,0 +1,67 @@ + +import sys + + +from iotAmak.environment import Environment +from fork import Fork + + +class PhiEnv(Environment): + + def __init__(self, broker_ip, nbr_phil): + self.nbr_phil = nbr_phil + super().__init__(broker_ip) + + def on_initialization(self): + self.forks = [] + for i in range(self.nbr_phil): + self.forks.append(Fork(i)) + self.subscribe("agent/" + str(i) + "/ask_spoon", self.ask_spoon) + self.subscribe("agent/" + str(i) + "/done_eating", self.done_eating) + + def ask_spoon(self, client, userdata, message): + res = str(message.payload.decode("utf-8")) + agent_id = int(str(message.topic).split("/")[1]) + + if res == "left": + fork_id = agent_id + else: + fork_id = (agent_id - 1) % self.nbr_phil + + if self.forks[fork_id].state == 1: + message = { + "response": "False" + } + elif self.forks[fork_id].taken_by == -1: + message = { + "response": "True", + "side": res, + "state": 0 + } + self.forks[fork_id].taken_by = agent_id + else: + message = { + "response": "True", + "side": res, + "state": 1 + } + self.forks[fork_id].taken_by = agent_id + self.forks[fork_id].state = 1 + + self.client.publish("env/agent/" + str(agent_id) + "/ask_spoon", str(message)) + + def done_eating(self, client, userdata, message): + agent_id = int(str(message.topic).split("/")[1]) + + for fork_id in [agent_id, (agent_id - 1) % self.nbr_phil]: + self.forks[fork_id].state = 0 + + def on_cycle_begin(self) -> None: + for fork in self.forks: + self.client.publish("env/fork/" + str(fork.identifier), str(fork.to_msg())) + print("Fork : ", fork.identifier," taken by ", fork.taken_by, " and is :", fork.state) + + +if __name__ == '__main__': + s = PhiEnv(str(sys.argv[1]), 5) + s.run() \ No newline at end of file diff --git a/uploads/media/philosophers.zip b/uploads/media/philosophers.zip new file mode 100644 index 0000000000000000000000000000000000000000..7c2deb36c8adeb6b9c9f9e60519c28d58b2724f3 Binary files /dev/null and b/uploads/media/philosophers.zip differ