diff --git a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py index b5ffae06cc56e4441f6b905413c9a933ed8b7b41..ce496da65f44bb3b05549795bda942c8ccaff097 100644 --- a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py +++ b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py @@ -159,7 +159,7 @@ to provide help, news and informations on your deployment. Or you can just ignor interface_auth_user = None) - # Jupyter Notebook + # Jupyter Notebook Container.objects.create(user = None, name = 'Jupyter Notebook', description = 'A Jupyter Notebook server', @@ -175,6 +175,37 @@ to provide help, news and informations on your deployment. Or you can just ignor supports_interface_auth = True, interface_auth_user = None) + # Official Jupyter Lab + Container.objects.create(user = None, + name = 'Jupyter Lab', + description = 'The official Jupyter Lab. The Scipy variant, which includes popular packages from the scientific Python ecosystem.', + registry = 'docker.io', + image = 'jupyter/scipy-notebook', + tag = 'lab-3.2.2', + arch = 'amd64,arm64', + os = 'linux', + interface_port = '8888', + interface_protocol = 'http', + interface_transport = 'tcp/ip', + supports_custom_interface_port = True, + supports_interface_auth = True) + + Container.objects.create(user = None, + name = 'Jupyter Lab', + description = 'The official Jupyter Lab. Includes popular packages from the scientific Python ecosystem.', + registry = 'docker.io', + image = 'jupyter/scipy-notebook', + tag = 'lab-3.1.17', + arch = 'amd64,arm64', + os = 'linux', + interface_port = '8888', + interface_protocol = 'http', + interface_transport = 'tcp/ip', + supports_custom_interface_port = True, + supports_interface_auth = True) + + + # SSH server Container.objects.create(user = None, name = 'SSH server', diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 77514a195336d0f778c95a0318ea50c6e6f4c07b..60159fa8403766ebeb4dd9b47332ac4c396910fd 100644 --- a/services/webapp/code/rosetta/core_app/models.py +++ b/services/webapp/code/rosetta/core_app/models.py @@ -144,10 +144,9 @@ class Container(models.Model): user_str = self.user.email if self.user else None return str('Container "{}" of user "{}" with image "{}" and tag "{}" on registry "{}" '.format(self.name, user_str, self.image, self.tag, self.registry)) - @ property def color(self): - string_int_hash = hash_string_to_int(self.image + self.tag + self.registry) + string_int_hash = hash_string_to_int(self.name + self.registry + self.image) color_map_index = string_int_hash % len(color_map) return color_map[color_map_index] diff --git a/services/webapp/code/rosetta/core_app/templates/components/computing.html b/services/webapp/code/rosetta/core_app/templates/components/computing.html index dfbbb418239939e88ea9fffd955dc5d78952ace3..76169573a7061ce6a86abce7035c661b074dc895 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/computing.html +++ b/services/webapp/code/rosetta/core_app/templates/components/computing.html @@ -115,6 +115,7 @@ {% csrf_token %} <input type="hidden" name="step" value="{{ data.next_step }}" /> <input type="hidden" name="task_container_uuid" value="{{container.uuid}}"> + <input type="hidden" name="task_container_arch" value="{{container_arch}}"> <input type="hidden" name="task_computing_uuid" value="{{computing.uuid}}"> <input type="submit" value="Choose" class="btn btn-connect"> </form> diff --git a/services/webapp/code/rosetta/core_app/templates/components/container.html b/services/webapp/code/rosetta/core_app/templates/components/container.html index d6981795f90ab97aa5196f0db2974db50e4fbb17..c4e167ac1049b59c2329a5fea10cb63748ea6ada 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/container.html +++ b/services/webapp/code/rosetta/core_app/templates/components/container.html @@ -1,6 +1,6 @@ - {% if details %} + {% if details %} <table class="dashboard" style="margin:10px; max-width:600px"> @@ -109,7 +109,7 @@ <div style="padding:10px; margin-top:5px; text-align:center; border-bottom: {{container.color}} solid 10px; "> - <a href="/software/?uuid={{ container.uuid }}">{{ container.name }}</a> + <a href="/software/container_?uuid={{ container.uuid }}">{{ container.name }}</a> </div> <div style="padding:10px; height: 110px; vertical-align: middle; "> @@ -125,7 +125,7 @@ <div class="image-version-box"> <!-- <font style="font-family:monospace; font-size:1.2em"></font> --> <b>Image:</b> <code>{{ container.image }}</code><br/> - <b>Tag:</b> {{ container.tag }} + <b>Tag:</b> {{ container.tag }} <b>Arch:</b> {{container_arch}} </div> </div> diff --git a/services/webapp/code/rosetta/core_app/templates/components/container_family.html b/services/webapp/code/rosetta/core_app/templates/components/container_family.html new file mode 100644 index 0000000000000000000000000000000000000000..043523a2b4304e2337a692f1a99fd174c73cbc69 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/templates/components/container_family.html @@ -0,0 +1,78 @@ + + {% if data.details %} + {% for container in container_family.members %} + {% include "components/container.html" with container=container details=data.details %} + {% endfor %} + + {% else %} + + <div style="width:300px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:15px"> + <form action="/new_task" method=GET> + <input type="hidden" name="step" value="two"> + + + <div style="padding:10px; margin-top:5px; text-align:center; border-bottom: {{container_family.color}} solid 10px; "> + <a href="/software/?container_family_id={{ container_family_id }}&details=True">{{ container_family.name }}</a> + </div> + + <div style="padding:10px; height: 125px; "> + <!-- <div style="position: relative; top: 50%; transform: translateY(-50%);"> --> + + {% if container_family.description %} + <div class="description-box" title="{{ container_family.description }}"> + {{ container_family.description }} + </div> + {% else %} + <br/> + {% endif %} + + <div class="image-version-box"> + <b>Image:</b> <code>{{ container_family.image }}</code><br/> + <div style="margin-top:2px"> + + <span style="vertical-align:top;"><b>Tag:</b> </span> + <select name="task_container_uuid" style="font-size:0.8em"> + {% for container in container_family.members %} + <option value="{{ container.uuid }}">{{ container.tag }}</option> + {% endfor %} + </select> + + <span style="vertical-align:top; margin-left:5px"><b>Arch:</b> </span> + <select name="task_container_arch" style="font-size:0.8em"> + <option value="auto" selected>auto</option> + + {% for arch in container_family.all_archs %} + <option value="{{ arch }}">{{ arch }}</option> + {% endfor %} + </select> + + + </div> + </div> + <!-- </div> --> + </div> + + <div style="margin-bottom:8px; margin-top: 3px; text-align:center"> + {% if not disable_play_button %} + + + <button type="submit" onclick="myFunction()" class="btn-link btn btn-light" style="border: #c0c0c0 1px solid"> + <i class="fa fa-play" style="color:green"></i> + </button> + + {% endif %} + </form> + </div> + + + + </div> + + {% endif %} + + + + + + + \ No newline at end of file diff --git a/services/webapp/code/rosetta/core_app/templates/components/container_family_mah.html b/services/webapp/code/rosetta/core_app/templates/components/container_family_mah.html new file mode 100644 index 0000000000000000000000000000000000000000..c2bf49b5e147b9e98f2e04c0b2a6f4e544a76c49 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/templates/components/container_family_mah.html @@ -0,0 +1,64 @@ + + + <div style="width:300px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:15px"> + <form action="/aaa" method=GET> + + + <div style="padding:10px; margin-top:5px; text-align:center; border-bottom: {{container.color}} solid 10px; "> + <a href="/software/?container_family={{ family_id }}">{{ container_family.name }}</a> + </div> + + <div style="padding:10px; height: 125px; "> + <!-- <div style="position: relative; top: 50%; transform: translateY(-50%);"> --> + + {% if container_family.description %} + <div class="description-box" title="{{ container_family.description }}"> + {{ container_family.description }} + </div> + {% else %} + <br/> + {% endif %} + + <div class="image-version-box"> + <b>Image:</b> <code>{{ container_family.image }}</code><br/> + <div style="margin-top:2px"> + + <span style="vertical-align:top;"><b>Tag:</b> </span> + <select name="task_container_uuid_and_arch" style="font-size:0.8em"> + {% for arch,container_by_tags in container_family.container_by_tags_by_arch.items %} + <optgroup label={{ arch }}> + {% for tag,container in container_by_tags.items %} + <option value="{{ container.uuid }}-{{ arch }}">{{ tag }}</option> + {% endfor %} + </optgroup> + {% endfor %} + </select> + + </div> + </div> + <!-- </div> --> + </div> + + <div style="margin-bottom:8px; margin-top: 3px; text-align:center"> + {% if not disable_play_button %} + + + <button type="submit" onclick="myFunction()" class="btn-link btn btn-light" style="border: #c0c0c0 1px solid"> + <i class="fa fa-play" style="color:green"></i> + </button> + + {% endif %} + </form> + </div> + + + + </div> + + + + + + + + \ No newline at end of file diff --git a/services/webapp/code/rosetta/core_app/templates/new_task.html b/services/webapp/code/rosetta/core_app/templates/new_task.html index 23db343aefb01a77fe92c45076ade8a1de3436fc..eef352b8585bded34d08027bad98e3fc5223e587 100644 --- a/services/webapp/code/rosetta/core_app/templates/new_task.html +++ b/services/webapp/code/rosetta/core_app/templates/new_task.html @@ -24,7 +24,7 @@ <div style="float:left; background:#ffffff; margin-left:10px; margin-right:10px; margin-top:0px; margin-bottom:20px"> <h4>Software container</h4> <div class="row" style="padding:5px"> - {% include "components/container.html" with container=data.task_container disable_play_button=True %} + {% include "components/container.html" with container=data.task_container container_arch=data.task_container_arch disable_play_button=True %} </div> </div> {% endif %} @@ -36,7 +36,7 @@ <h4>Computing resource</h4> <div class="row" style="padding:5px"> {% for computing in data.computings %} - {% include "components/computing.html" with container=data.task_container %} + {% include "components/computing.html" with container=data.task_container container_arch=data.task_container_arch %} {% endfor %} </div> </div> @@ -76,6 +76,7 @@ <form action="/new_task/" method="POST"> {% csrf_token %} <input type="hidden" name="task_container_uuid" value="{{data.task_container.uuid}}"> + <input type="hidden" name="task_container_arch" value="{{data.task_container_arch}}"> <input type="hidden" name="step" value="{{ data.next_step }}" /> <input type="hidden" name="task_name" value="{{ data.task_name }}" /> <input type="hidden" name="task_container_uuid" value="{{ data.task_container.uuid }}" /> diff --git a/services/webapp/code/rosetta/core_app/templates/software.html b/services/webapp/code/rosetta/core_app/templates/software.html index 8be03d0b5deed876bd26394f2d26f618dc23ec76..b5094966c39a8706b89c225b1291705942bcefc2 100644 --- a/services/webapp/code/rosetta/core_app/templates/software.html +++ b/services/webapp/code/rosetta/core_app/templates/software.html @@ -9,8 +9,12 @@ <div class="dashboard"> <div class="span8 offset2"> - {% if data.container %} + {% if data.details %} + {% if data.container_families %} + <h1><a href="/software">Software containers</a> <span style="font-size:18px"> / {{ data.containers.0.name }}</span></h1> + {% else %} <h1><a href="/software">Software containers</a> <span style="font-size:18px"> / {{ data.container.name }}</span></h1> + {% endif %} {% else %} {% if data.mode == 'new_task' %} <h1>New Task</h1> @@ -65,10 +69,16 @@ <div class="row" style="padding:5px"> {% if data.container %} {% include "components/container.html" with container=data.container details=True %} - {% else %} + {% else %} + {% if data.container_families %} + {% for container_family_id, container_family in data.container_families.items %} + {% include "components/container_family.html" with container_family=container_family container_family_id=container_family_id%} + {% endfor %} + {% else %} {% for container in data.containers %} {% include "components/container.html" with container=container %} {% endfor %} + {% endif %} {% endif %} </div> diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py index 80f454208e16173b060ec5e5af49b827e1fca4ef..4b56e040041caefd9f8d1f819b3fe2d8621305f7 100644 --- a/services/webapp/code/rosetta/core_app/views.py +++ b/services/webapp/code/rosetta/core_app/views.py @@ -2,6 +2,7 @@ import os import uuid import json import subprocess +import base64 from django.conf import settings from django.shortcuts import render from django.contrib.auth import authenticate, login, logout @@ -10,7 +11,7 @@ from django.contrib.auth.models import User from django.shortcuts import redirect from django.db.models import Q from .models import Profile, LoginToken, Task, TaskStatuses, Container, Computing, KeyPair, Page -from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_task_tunnel_host, get_task_proxy_host, random_username, setup_tunnel_and_proxy, finalize_user_creation +from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_task_tunnel_host, get_task_proxy_host, random_username, setup_tunnel_and_proxy, finalize_user_creation, get_md5 from .decorators import public_view, private_view from .exceptions import ErrorMessage @@ -463,6 +464,14 @@ def new_task(request): raise Exception('Consistency error, container with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_container_uuid, request.user.email)) return task_container + # Get task container arch helper function + def get_task_container_arch(request): + container_arch = request.POST.get('task_container_arch', None) + if not container_arch: + # At the second step the task uuid is set via a GET request + container_arch = request.GET.get('task_container_arch', None) + return container_arch + # Get task computing helper function def get_task_computing(request): task_computing_uuid = request.POST.get('task_computing_uuid', None) @@ -496,8 +505,9 @@ def new_task(request): elif step == 'two': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) + data['task_container_arch'] = get_task_container_arch(request) # List all computing resources data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user)) @@ -507,8 +517,9 @@ def new_task(request): elif step == 'three': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) + data['task_container_arch'] = get_task_container_arch(request) # Get computing resource data['task_computing'] = get_task_computing(request) @@ -523,8 +534,9 @@ def new_task(request): elif step == 'last': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) + data['task_container_arch'] = get_task_container_arch(request) # Get computing resource data['task_computing'] = get_task_computing(request) @@ -691,8 +703,11 @@ def software(request): data['profile'] = Profile.objects.get(user=request.user) # Get action if any - uuid = request.GET.get('uuid', None) + container_uuid = request.GET.get('container_uuid', None) + container_family_id = request.GET.get('container_family_id', None) action = request.GET.get('action', None) + details = booleanize(request.GET.get('details', False)) + # Get filter/search if any search_text = request.POST.get('search_text', '') @@ -701,6 +716,7 @@ def software(request): # Set back to page data data['search_owner'] = search_owner data['search_text'] = search_text + data['details'] = details # Are we using this page as first step of a new task? data['mode'] = request.GET.get('mode', None) @@ -708,14 +724,14 @@ def software(request): data['mode'] = request.POST.get('mode', None) - # Do we have to operate on a specific container? - if uuid: + # Do we have to operate on a specific container, or family of containers? + if container_uuid: try: # Get the container (raises if none available including no permission) try: - container = Container.objects.get(uuid=uuid) + container = Container.objects.get(uuid=container_uuid) except Container.DoesNotExist: raise ErrorMessage('Container does not exists or no access rights') if container.user and container.user != request.user: @@ -735,26 +751,83 @@ def software(request): data['error'] = 'Error in getting the software container or performing the required action' logger.error('Error in getting container with uuid="{}" or performing the required action: "{}"'.format(uuid, e)) return render(request, 'error.html', {'data': data}) - - - # Get containers (fitered by search term, or all) - if search_text: - search_query=(Q(name__icontains=search_text) | Q(description__icontains=search_text) | Q(image__icontains=search_text)) - user_containers = Container.objects.filter(search_query, user=request.user) - platform_containers = Container.objects.filter(search_query, user=None) - else: - user_containers = Container.objects.filter(user=request.user) - platform_containers = Container.objects.filter(user=None) - # Filter by owner + # Or, do we have to operate on a container family? + elif container_family_id: + + # Get back name, registry and image from contsainer url + container_name, container_registry, container_image = base64.b64decode(container_family_id.encode('utf8')).decode('utf8').split('\t') + + # get containers from the DB + user_containers = Container.objects.filter(user=request.user, name=container_name, registry=container_registry, image=container_image) + platform_containers = Container.objects.filter(user=None, name=container_name, registry=container_registry, image=container_image) + + else: + + # Get containers (fitered by search term, or all) + if search_text: + search_query=(Q(name__icontains=search_text) | Q(description__icontains=search_text) | Q(image__icontains=search_text)) + user_containers = Container.objects.filter(search_query, user=request.user) + platform_containers = Container.objects.filter(search_query, user=None) + else: + user_containers = Container.objects.filter(user=request.user) + platform_containers = Container.objects.filter(user=None) + + + # Ok, nilter by owner if search_owner != 'All': if search_owner == 'User': platform_containers =[] if search_owner == 'Platform': user_containers = [] - + + # Create all container list data['containers'] = list(user_containers) + list(platform_containers) - + + # Merge containers with the same name, registry and image + data['container_families'] = {} + + # Container family support class + class ContainerFamily(object): + + def __init__(self, id, name, registry, image): + self.id = id + self.name = name + self.registry = registry + self.image = image + self.description = None + self.members = [] + self.all_archs = [] + self.container_by_tags_by_arch = {} + + def add(self, container): + self.members.append(container) + + if not self.description: + self.description = container.description + + for arch in container.arch.split(','): + + if not arch in self.all_archs: + self.all_archs.append(arch) + if not arch in self.container_by_tags_by_arch: + self.container_by_tags_by_arch[arch]={} + self.container_by_tags_by_arch[arch][container.tag] = container + + @ property + def color(self): + try: + return self.members[0].color + except IndexError: + return '#000000' + + # Populate container families + for container in data['containers']: + container_family_id = base64.b64encode('{}\t{}\t{}'.format(container.name, container.registry, container.image).encode('utf8')).decode('utf8') + if container_family_id not in data['container_families']: + data['container_families'][container_family_id] = ContainerFamily(container_family_id, container.name, container.registry, container.image) + data['container_families'][container_family_id].add(container) + return render(request, 'software.html', {'data': data})