diff --git a/images/webapp/code/rosetta/base_app/admin.py b/images/webapp/code/rosetta/base_app/admin.py index 23e4267e97134ae03de40f769bda7d1ae34f20b4..2588ae2a18ca958f8ca93552e0f576d886077ef4 100644 --- a/images/webapp/code/rosetta/base_app/admin.py +++ b/images/webapp/code/rosetta/base_app/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from .models import Profile, LoginToken, Task +from .models import Profile, LoginToken, Task, Container admin.site.register(Profile) admin.site.register(LoginToken) admin.site.register(Task) +admin.site.register(Container) diff --git a/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py index d7600e23ab76f35dc01a864bd54cc54eb41b8f61..16a9d65ac2ca63ba890b9e04f17d5e8a80b9c561 100644 --- a/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py +++ b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py @@ -1,12 +1,13 @@ from django.core.management.base import BaseCommand from django.contrib.auth.models import User -from ...models import Profile +from ...models import Profile, Container class Command(BaseCommand): help = 'Adds the admin superuser with \'a\' password.' def handle(self, *args, **options): + # Admin try: User.objects.get(username='admin') print('Not creating admin user as it already exist') @@ -14,7 +15,8 @@ class Command(BaseCommand): print('Creating admin user with default password') admin = User.objects.create_superuser('admin', 'admin@example.com', 'admin') Profile.objects.create(user=admin) - + + # Testuser try: User.objects.get(username='testuser') print('Not creating test user as it already exist') @@ -22,4 +24,38 @@ class Command(BaseCommand): print('Creating test user with default password') testuser = User.objects.create_user('testuser', 'testuser@rosetta.platform', 'testpass') Profile.objects.create(user=testuser, authtoken='129aac94-284a-4476-953c-ffa4349b4a50') - \ No newline at end of file + + # public containers + public_containers = Container.objects.filter(user=None) + if public_containers: + print('Not creating public containers as they already exist') + else: + print('Creating public containers...') + + # MetaDesktop Docker + Container.objects.create(user = None, + image = 'rosetta/metadesktop', + type = 'docker', + registry = 'docker_local', + service_ports = '8590') + + # Astrocook + Container.objects.create(user = None, + image = 'sarusso/astrocook:b2b819e', + type = 'docker', + registry = 'docker_local', + service_ports = '8590') + + + + + + + + + + + + + + diff --git a/images/webapp/code/rosetta/base_app/models.py b/images/webapp/code/rosetta/base_app/models.py index 8cf6770d2c4d1c9ac8c9870400919d72772455dd..6c1603c9ca1f6910bee5e1fcec50c7f95f535699 100644 --- a/images/webapp/code/rosetta/base_app/models.py +++ b/images/webapp/code/rosetta/base_app/models.py @@ -55,7 +55,6 @@ class Task(models.Model): tid = models.CharField('Task ID', max_length=64, blank=False, null=False) uuid = models.CharField('Task UUID', max_length=36, blank=False, null=False) name = models.CharField('Task name', max_length=36, blank=False, null=False) - container = models.CharField('Task container', max_length=36, blank=False, null=False) status = models.CharField('Task status', max_length=36, blank=True, null=True) created = models.DateTimeField('Created on', default=timezone.now) compute = models.CharField('Task compute', max_length=36, blank=True, null=True) @@ -64,6 +63,9 @@ class Task(models.Model): ip = models.CharField('Task ip address', max_length=36, blank=True, null=True) tunnel_port = models.IntegerField('Task tunnel port', blank=True, null=True) + # Links + container = models.ForeignKey('Container', on_delete=models.CASCADE, related_name='+') + def save(self, *args, **kwargs): try: @@ -106,7 +108,21 @@ class Task(models.Model): return self.uuid.split('-')[0] +#========================= +# Containers +#========================= +class Container(models.Model): + #uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE, null=True) + # If a container has no user, it will be available to anyone. Can be created, edited and deleted only by admins. + image = models.CharField('Container image', max_length=255, blank=False, null=False) + type = models.CharField('Container type', max_length=36, blank=False, null=False) + registry = models.CharField('Container registry', max_length=255, blank=False, null=False) + service_ports = models.CharField('Container service ports', max_length=36, blank=True, null=True) + #private = models.BooleanField('Container is private and needs auth to be pulled from the registry') + def __str__(self): + return str('Container of type "{}" with image "{}" from registry "{}" of user "{}"'.format(self.type, self.image, self.registry, self.user)) diff --git a/images/webapp/code/rosetta/base_app/templates/add_container.html b/images/webapp/code/rosetta/base_app/templates/add_container.html new file mode 100644 index 0000000000000000000000000000000000000000..0ca208b8a3f3d90a197d5e165faf5021e95891df --- /dev/null +++ b/images/webapp/code/rosetta/base_app/templates/add_container.html @@ -0,0 +1,103 @@ +{% load static %} +{% include "header.html" %} +{% include "navigation.html" with main_path='/main/' %} + +<br/> +<br/> + +<div class="container"> + <div class="dashboard"> + <div class="span8 offset2"> + <h1>Add container</h1> + <hr> + + {% if not data.added %} + + <h3>Configure the new container.</h3> + + <br/> + + <form action="#" method="POST"> + {% csrf_token %} + + <table class="dashboard" style="max-width:430px"> + + <tr> + <td><b>Type</b></td><td> + <select name="container_type" > + <option value="docker" selected>Docker</option> + <option value="singularity">Singularity</option> + </select> + </td> + </tr> + + <tr> + <td><b>Registry</b></td><td> + <select name="container_registry" > + <option value="docker_local" selected>Local</option> + <option value="docker_hub">Docker Hub</option> + <option value="singularityhub">Singularity Hub</option> + </select> + </td> + </tr> + + <tr> + <td><b>Container image</b></td> + <td> + <input type="text" name="container_image" value="" placeholder="" size="23" required /> + </td> + </tr> + + <tr> + <td><b>Service port(s)</b></td> + <td> + <input type="text" name="container_service_ports" value="" placeholder="" size="5" required /> + </td> + </tr> + + <tr> + <td colspan=2 align=center style="padding:20px"> + <input type="submit" value="Add"> + </td> + </tr> + </table> + </form> + + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + + + + + + {% else %} + Ok, Container added. Go back to your <a href="/tasks">tasks list</a>. + + + {% endif %} + + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + + </div> + </div> +</div> + +{% include "footer.html" %} + + + + + + diff --git a/images/webapp/code/rosetta/base_app/templates/containers.html b/images/webapp/code/rosetta/base_app/templates/containers.html new file mode 100644 index 0000000000000000000000000000000000000000..50d5415376bf51878ad50a0abc6decfbe77171fb --- /dev/null +++ b/images/webapp/code/rosetta/base_app/templates/containers.html @@ -0,0 +1,88 @@ +{% load static %} +{% include "header.html" %} +{% include "navigation.html" with main_path='/main/' %} + +<br/> +<br/> + +<div class="container"> + <div class="dashboard"> + <div class="span8 offset2"> + <h1>Containers List</h1> + <hr/> + + + {% for container in data.platform_containers %} + <table class="dashboard"> + + <tr> + <td><b>Container image</b></td> + <td>{{ container.image }} (platform)</td> + </tr> + + <tr> + <td><b>Container type</b></td> + <td>{{ container.type }}</td> + </tr> + + <tr> + <td><b>Container registry</b></td> + <td>{{ container.registry }}</td> + </tr> + + <tr> + <td><b>Container service ports</b></td> + <td>{{ container.service_ports}}</td> + </tr> + + </table> + <br /> + {% endfor %} + + + {% for container in data.user_containers %} + <table class="dashboard"> + + <tr> + <td><b>Container image</b></td> + <td>{{ container.image }} (user)</td> + </tr> + + <tr> + <td><b>Container type</b></td> + <td>{{ container.type }}</td> + </tr> + + <tr> + <td><b>Container registry</b></td> + <td>{{ container.registry }}</td> + </tr> + + <tr> + <td><b>Container service ports</b></td> + <td>{{ container.service_ports}}</td> + </tr> + + </table> + <br /> + {% endfor %} + + <br /> + <a href="/create_container">Add new...</a> + <br/> + <br/> + <br/> + <br/> + <br/> + <br/> + + </div> + </div> +</div> + +{% include "footer.html" %} + + + + + diff --git a/images/webapp/code/rosetta/base_app/templates/create_task.html b/images/webapp/code/rosetta/base_app/templates/create_task.html index c3bf5cf66bc13cb0c8754cd190b18ac12fe873da..4148c3dad080484071e30c0ed0a9202bf53bfde8 100644 --- a/images/webapp/code/rosetta/base_app/templates/create_task.html +++ b/images/webapp/code/rosetta/base_app/templates/create_task.html @@ -11,7 +11,7 @@ <h1>New Task</h1> <hr> - {% if not data.name %} + {% if not data.created %} <h3>Choose a name and a type for your new Task.</h3> @@ -20,12 +20,12 @@ <form action="/create_task/" method="POST"> {% csrf_token %} - <table class="dashboard" style="max-width:430px"> + <table class="dashboard" style="max-width:600px"> <tr> <td><b>Task name </b></td> <td> - <input type="text" name="name" value="" placeholder="" size="23" required /> + <input type="text" name="task_name" value="" placeholder="" size="23" required /> </td> </tr> @@ -46,21 +46,30 @@ <tr> <td><b>Task container</b></td><td> - <select name="container" > - <option value="metadesktop" selected>Meta Desktop</option> + <select name="task_container_id" > + <!-- <option value="metadesktop" selected>Meta Desktop</option> <option value="astroccok">Astrocook</option> - <option value="gadgetviewer">Gadget Viewer</option> - </select> + <option value="gadgetviewer">Gadget Viewer</option> --> + {% for container in data.platform_containers %} + <option value="{{container.id}}">{{container.image}} (platform)</option> --> + {% endfor %} + {% for container in data.user_containers %} + <option value="{{container.id}}">{{container.image}} (user)</option> --> + {% endfor %} + + </select> + | <a href="/add_container">Add new...</a> </td> </tr> <tr> <td><b>Computing resource</b></td><td> - <select name="compute" > + <select name="task_compute" > <option value="local" selected>Local</option> <option value="demoremote">Demo remote</option> <option value="demoslurm">Demo Slurm cluster</option> - </select> + </select> + | <a href="/add_compute">Add new...</a> </td> </tr> diff --git a/images/webapp/code/rosetta/base_app/templates/navigation.html b/images/webapp/code/rosetta/base_app/templates/navigation.html index 38e5cdb91ede54ae8b609956c2770f112e1158ed..b7be43800bf3a7c08f32f015db4ee6d181d70925 100644 --- a/images/webapp/code/rosetta/base_app/templates/navigation.html +++ b/images/webapp/code/rosetta/base_app/templates/navigation.html @@ -22,6 +22,9 @@ {% if user.is_authenticated %} + <li> + <a href="/containers" onclick = $("#menu-close").click(); >Containers</a> + </li> <li> <a href="/tasks" onclick = $("#menu-close").click(); >Tasks</a> </li> diff --git a/images/webapp/code/rosetta/base_app/templates/tasks.html b/images/webapp/code/rosetta/base_app/templates/tasks.html index 25e914535e3bc8b47192e4906112ba64f8e33a39..743bc1220647d45f14fe1198b4524fa1f70261c1 100644 --- a/images/webapp/code/rosetta/base_app/templates/tasks.html +++ b/images/webapp/code/rosetta/base_app/templates/tasks.html @@ -32,7 +32,7 @@ <tr> <td><b>Task container</b></td> - <td>{{ task.container }}</td> + <td>{{ task.container.image }}</td> </tr> <tr> @@ -45,10 +45,10 @@ <td>{{ task.created }}</td> </tr> - <tr> + <!-- <tr> <td><b>Task pid</b></td> <td>{{ task.pid}}</td> - </tr> + </tr> --> <tr> <td><b>Task ip</b></td> diff --git a/images/webapp/code/rosetta/base_app/views.py b/images/webapp/code/rosetta/base_app/views.py index 45dae1fe174282652a4135df6f34fa32e45cc49c..286e8356c5efd1f68b7e68bbb935f8288a39c7f6 100644 --- a/images/webapp/code/rosetta/base_app/views.py +++ b/images/webapp/code/rosetta/base_app/views.py @@ -20,7 +20,7 @@ from django.contrib.auth.models import User from django.contrib.auth import update_session_auth_hash # Project imports -from .models import Profile, LoginToken, Task, TaskStatuses +from .models import Profile, LoginToken, Task, TaskStatuses, Container from .utils import send_email, format_exception, random_username, log_user_activity, timezonize, os_shell # Setup logging @@ -31,7 +31,8 @@ logger = logging.getLogger(__name__) from .exceptions import ErrorMessage, ConsistencyException # Conf -SUPPORTED_TASK_TYPES = ['metadesktop', 'astrocook', 'gadgetviewer'] +SUPPORTED_CONTAINER_TYPES = ['docker', 'singularity'] +SUPPORTED_REGISTRIES = ['docker_local', 'docker_hub', 'signularity_hub'] TASK_DATA_DIR = "/data" #========================= @@ -566,27 +567,32 @@ def create_task(request): data['user'] = request.user data['profile'] = Profile.objects.get(user=request.user) data['title'] = 'New Task' - data['name'] = request.POST.get('name',None) - if data['name']: - - # Type - data['container'] = request.POST.get('container', None) - if not data['container']: - data['error'] = 'No container given' - return render(request, 'error.html', {'data': data}) + # Get containers configured on the platform, both private to this user and public + data['platform_containers'] = Container.objects.filter(user=request.user) + data['user_containers'] = Container.objects.filter(user=None) - if not data['container'] in SUPPORTED_TASK_TYPES: - data['error'] = 'No valid task container' - return render(request, 'error.html', {'data': data}) - - compute = request.POST.get('compute', None) + # Task name if any + task_name = request.POST.get('task_name', None) - logger.debug(compute) + if task_name: + + # Task container + task_container_id = request.POST.get('task_container_id', None) - if compute not in ['local', 'demoremote']: - data['error'] = 'Unknown compute resource "{}'.format(compute) - return render(request, 'error.html', {'data': data}) + # Get the container object, first try as public and then as private + try: + task_container = Container.objects.get(id=task_container_id, user=None) + except Container.DoesNotExist: + try: + task_container = Container.objects.get(id=task_container_id, user=request.user) + except Container.DoesNotExist: + raise Exception('Consistency error, container with id "{}" does not exists or user "{}" does not have access rights'.format(task_container_id, request.user.email)) + + # Compute + task_compute = request.POST.get('task_compute', None) + if task_compute not in ['local', 'demoremote']: + raise ErrorMessage('Unknown compute resource "{}') # Generate the task uuid str_uuid = str(uuid.uuid4()) @@ -595,14 +601,15 @@ def create_task(request): # Create the task object task = Task.objects.create(uuid = str_uuid, user = request.user, - name = data['name'], + name = task_name, status = TaskStatuses.created, - container = data['container'], - compute = compute) + container = task_container, + compute = task_compute) + # Actually start tasks try: - if compute == 'local': + if task_compute == 'local': # Get our ip address #import netifaces @@ -615,13 +622,15 @@ def create_task(request): # Data volume run_command += ' -v {}/task-{}:/data'.format(TASK_DATA_DIR, str_shortuuid) - # Host name, image entry command - task_container = 'task-{}'.format(data['container']) - run_command += ' -h task-{} -d -t localhost:5000/rosetta/metadesktop'.format(str_shortuuid, task_container) - - # Create the model - task = Task.objects.create(user=request.user, name=data['name'], status=TaskStatuses.created, container=data['container']) + # Set registry string + if task.container.registry == 'local': + registry_string = 'localhost:5000/' + else: + registry_string = '' + # Host name, image entry command + run_command += ' -h task-{} -d -t {}{}'.format(str_shortuuid, registry_string, task.container.image) + # Run the task Debug logger.debug('Running new task with command="{}"'.format(run_command)) out = os_shell(run_command, capture=True) @@ -647,7 +656,7 @@ def create_task(request): # Save task.save() - elif compute == 'demoremote': + elif task_compute == 'demoremote': logger.debug('Using Demo Remote as compute resource') @@ -680,19 +689,125 @@ def create_task(request): else: - raise Exception('Consistency exception: invalid compute resource "{}'.format(compute)) + raise Exception('Consistency exception: invalid compute resource "{}'.format(task_compute)) except Exception as e: data['error'] = 'Error in creating new Task.' logger.error(e) return render(request, 'error.html', {'data': data}) + # Set created switch + data['created'] = True return render(request, 'create_task.html', {'data': data}) +#========================= +# Containers +#========================= + +@private_view +def containers(request): + + # Init data + data={} + data['user'] = request.user + data['profile'] = Profile.objects.get(user=request.user) + data['title'] = 'Add compute' + data['name'] = request.POST.get('name',None) + + # Get containers configured on the platform, both private to this user and public + data['platform_containers'] = Container.objects.filter(user=request.user) + data['user_containers'] = Container.objects.filter(user=None) + + return render(request, 'containers.html', {'data': data}) + + + +#========================= +# Add Container view +#========================= + +@private_view +def add_container(request): + + # Init data + data={} + data['user'] = request.user + data['profile'] = Profile.objects.get(user=request.user) + data['title'] = 'Add container' + + # Container image if any + container_image = request.POST.get('container_image',None) + + if container_image: + + # Container type + container_type = request.POST.get('container_type', None) + if not container_type: + raise ErrorMessage('No container type given') + if not container_type in SUPPORTED_CONTAINER_TYPES: + raise ErrorMessage('No valid container type') + + # Container registry + container_registry = request.POST.get('container_registry', None) + if not container_registry: + raise ErrorMessage('No registry type given') + if not container_registry in SUPPORTED_REGISTRIES: + raise ErrorMessage('No valid registry') + + # Container service ports + container_service_ports = request.POST.get('container_service_ports', None) + + # Log + logger.debug('Creating new container object with image="{}", type="{}", registry="{}", service_ports="{}"'.format(container_image, container_type, container_registry, container_service_ports)) + + # Create + Container.objects.create(user = request.user, + image = container_image, + type = container_type, + registry = container_registry, + service_ports = container_service_ports) + # Set added switch + data['added'] = True + + return render(request, 'add_container.html', {'data': data}) + + + +#========================= +# Computes view +#========================= + +@private_view +def computes(request): + + # Init data + data={} + data['user'] = request.user + data['profile'] = Profile.objects.get(user=request.user) + data['title'] = 'Add compute' + data['name'] = request.POST.get('name',None) + + + return render(request, 'computes.html', {'data': data}) + +#========================= +# Add Compute view +#========================= + +@private_view +def add_compute(request): + # Init data + data={} + data['user'] = request.user + data['profile'] = Profile.objects.get(user=request.user) + data['title'] = 'Add compute' + data['name'] = request.POST.get('name',None) + + return render(request, 'add_compute.html', {'data': data}) diff --git a/images/webapp/code/rosetta/urls.py b/images/webapp/code/rosetta/urls.py index 75842df91c19c2614dc6cd68546a57e49d51382d..694dc8f508bbb5b8f7929781a04e9b11c6f312ff 100644 --- a/images/webapp/code/rosetta/urls.py +++ b/images/webapp/code/rosetta/urls.py @@ -42,6 +42,10 @@ urlpatterns = [ url(r'^account/$', base_app_views.account), url(r'^tasks/$', base_app_views.tasks), url(r'^create_task/$', base_app_views.create_task), + url(r'^computes/$', base_app_views.computes), + url(r'^add_compute/$', base_app_views.add_compute), + url(r'^containers/$', base_app_views.containers), + url(r'^add_container/$', base_app_views.add_container), # Modules path('admin/', admin.site.urls),