diff --git a/services/webapp/code/rosetta/core_app/computing_managers.py b/services/webapp/code/rosetta/core_app/computing_managers.py index 6fe2a40ff6fc53eba3dfaf8c8137617c970f379c..e130bb85c3c9ae2d1b1b72390fde8ee39a87836f 100644 --- a/services/webapp/code/rosetta/core_app/computing_managers.py +++ b/services/webapp/code/rosetta/core_app/computing_managers.py @@ -111,7 +111,7 @@ class InternalSingleNodeComputingManager(SingleNodeComputingManager): #run_command += ' -v {}/user-{}:/data'.format(settings.LOCAL_USER_DATA_DIR, task.user.id) # Host name, image entry command - run_command += ' -h task-{} -d -t {}/{}:{}'.format(task.uuid, task.container.registry, task.container.image, task.container.tag) + run_command += ' -h task-{} -d -t {}/{}:{}'.format(task.uuid, task.container.registry, task.container.image_name, task.container.image_tag) # Debug logger.debug('Running new task with command="{}"'.format(run_command)) @@ -238,7 +238,7 @@ class SSHSingleNodeComputingManager(SingleNodeComputingManager, SSHComputingMana run_command += 'exec nohup singularity run {} --pid --writable-tmpfs --no-home --home=/home/metauser --workdir /tmp/{}_data/tmp -B/tmp/{}_data/home:/home --containall --cleanenv '.format(binds, task.uuid, task.uuid) # Container part - run_command+='docker://{}/{}:{} &>> /tmp/{}_data/task.log & echo \$!"\''.format(task.container.registry, task.container.image, task.container.tag, task.uuid) + run_command+='docker://{}/{}:{} &>> /tmp/{}_data/task.log & echo \$!"\''.format(task.container.registry, task.container.image_name, task.container.image_tag, task.uuid) else: @@ -377,7 +377,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag run_command += 'exec nohup singularity run {} --pid --writable-tmpfs --no-home --home=/home/metauser --workdir /tmp/{}_data/tmp -B/tmp/{}_data/home:/home --containall --cleanenv '.format(binds, task.uuid, task.uuid) # Double to escape for Python, six for shell (double times three as \\\ escapes a single slash in shell) - run_command+='docker://{}/{}:{} &> \$HOME/{}.log\\" > \$HOME/{}.sh && sbatch {} \$HOME/{}.sh"\''.format(task.container.registry, task.container.image, task.container.tag, task.uuid, task.uuid, sbatch_args, task.uuid) + run_command+='docker://{}/{}:{} &> \$HOME/{}.log\\" > \$HOME/{}.sh && sbatch {} \$HOME/{}.sh"\''.format(task.container.registry, task.container.image_name, task.container.image_tag, task.uuid, task.uuid, sbatch_args, task.uuid) else: raise NotImplementedError('Default container runtime "{}" not supported'.format(task.computing.default_container_runtime)) 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 3084c8ebad09df303f130f13d7e3a94b1df8760b..a945d05a3f0f50ba4d15023e6f40b5698a347034 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 @@ -132,12 +132,12 @@ to provide help, news and informations on your deployment. Or you can just ignor name = 'Minimal Desktop ', description = 'A minimal desktop environment providing basic window management functionalities and a terminal.', registry = 'docker.io', - image = 'sarusso/minimaldesktop', - tag = 'v0.2.0', - arch = 'amd64', - os = 'linux', - interface_port = '8590', - interface_protocol = 'http', + image_name = 'sarusso/minimaldesktop', + image_tag = 'v0.2.0', + image_arch = 'amd64', + image_os = 'linux', + interface_port = '8590', + interface_protocol = 'http', interface_transport = 'tcp/ip', supports_custom_interface_port = True, supports_interface_auth = True) @@ -147,12 +147,12 @@ to provide help, news and informations on your deployment. Or you can just ignor name = 'Basic Desktop', description = 'A basic desktop environment. Provides a terminal, a file manager, a web browser and other generic applications.', registry = 'docker.io', - image = 'sarusso/basicdesktop', - tag = 'v0.2.0', - arch = 'amd64', - os = 'linux', - interface_port = '8590', - interface_protocol = 'http', + image_name = 'sarusso/basicdesktop', + image_tag = 'v0.2.0', + image_arch = 'amd64', + image_os = 'linux', + interface_port = '8590', + interface_protocol = 'http', interface_transport = 'tcp/ip', supports_custom_interface_port = True, supports_interface_auth = True, @@ -164,46 +164,48 @@ to provide help, news and informations on your deployment. Or you can just ignor name = 'Jupyter Notebook', description = 'A Jupyter Notebook server', registry = 'docker.io', - image = 'sarusso/jupyternotebook', - tag = 'v0.2.0', - arch = 'amd64', - os = 'linux', - interface_port = '8888', - interface_protocol = 'http', + image_name = 'sarusso/jupyternotebook', + image_tag = 'v0.2.0', + image_arch = 'amd64', + image_os = 'linux', + interface_port = '8888', + interface_protocol = 'http', interface_transport = 'tcp/ip', supports_custom_interface_port = True, 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) - + # Official Jupyter containers + for tag in ['lab-3.2.2', 'lab-3.1.17']: + + Container.objects.create(user = None, + name = 'Jupyter Data Science Lab', + description = 'The official Jupyter Lab. The Data Science variant, which includes libraries for data analysis from the Julia, Python, and R communities.', + registry = 'docker.io', + image_name = 'jupyter/scipy-notebook', + image_tag = tag, + image_arch = None, + image_os = None, + interface_port = '8888', + interface_protocol = 'http', + interface_transport = 'tcp/ip', + supports_custom_interface_port = True, + supports_interface_auth = True) + + for arch in ['amd64', 'arm64']: + 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_name = 'jupyter/scipy-notebook', + image_tag = tag, + image_arch = arch, + image_os = 'linux', + interface_port = '8888', + interface_protocol = 'http', + interface_transport = 'tcp/ip', + supports_custom_interface_port = True, + supports_interface_auth = True) # SSH server @@ -211,10 +213,10 @@ to provide help, news and informations on your deployment. Or you can just ignor name = 'SSH server', description = 'An SSH server supporting X forwarding as well.', registry = 'docker.io', - image = 'sarusso/ssh', - tag = 'v0.2.0', - arch = 'amd64', - os = 'linux', + image_name = 'sarusso/ssh', + image_tag = 'v0.2.0', + image_arch = 'amd64', + image_os = 'linux', interface_port = '22', interface_protocol = 'ssh', interface_transport = 'tcp/ip', diff --git a/services/webapp/code/rosetta/core_app/migrations/0025_auto_20211122_1332.py b/services/webapp/code/rosetta/core_app/migrations/0025_auto_20211122_1332.py new file mode 100644 index 0000000000000000000000000000000000000000..4be61771093dc932cb2d367e45736f604c04f3d9 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0025_auto_20211122_1332.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.1 on 2021-11-22 13:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0024_computing_emulated_archs'), + ] + + operations = [ + migrations.RenameField( + model_name='container', + old_name='arch', + new_name='image_arch', + ), + migrations.RenameField( + model_name='container', + old_name='image', + new_name='image_name', + ), + migrations.RenameField( + model_name='container', + old_name='os', + new_name='image_os', + ), + migrations.RenameField( + model_name='container', + old_name='tag', + new_name='image_tag', + ), + migrations.AddField( + model_name='container', + name='image_digest', + field=models.CharField(blank=True, max_length=96, null=True, verbose_name='SHA 256 digest'), + ), + ] diff --git a/services/webapp/code/rosetta/core_app/migrations/0026_auto_20211123_0037.py b/services/webapp/code/rosetta/core_app/migrations/0026_auto_20211123_0037.py new file mode 100644 index 0000000000000000000000000000000000000000..2e335e86ceaf3ab1ca80d72d76e635e1dd794e43 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0026_auto_20211123_0037.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.1 on 2021-11-23 00:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0025_auto_20211122_1332'), + ] + + operations = [ + migrations.AlterField( + model_name='container', + name='image_arch', + field=models.CharField(blank=True, max_length=36, null=True, verbose_name='Architecture'), + ), + migrations.AlterField( + model_name='container', + name='image_os', + field=models.CharField(blank=True, max_length=36, null=True, verbose_name='Operating system'), + ), + ] diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 587377fbc3e5a4d7e01ce50702599272699d8fb9..c8eaa4568cd496dc9a4743b9c0c35b9e7b902c8b 100644 --- a/services/webapp/code/rosetta/core_app/models.py +++ b/services/webapp/code/rosetta/core_app/models.py @@ -1,5 +1,6 @@ import uuid import json +import base64 from django.conf import settings from django.db import models from django.contrib.auth.models import User, Group @@ -110,19 +111,22 @@ class Container(models.Model): group = models.ForeignKey(Group, related_name='containers', on_delete=models.CASCADE, blank=True, null=True) # If a container has no group, it will be available to anyone. Can be created, edited and deleted only by admins. - # Generic attributes name = models.CharField('Name', max_length=255, blank=False, null=False) description = models.TextField('Description', blank=True, null=True) - # Registry-related attributes + # Registry registry = models.CharField('Registry', max_length=255, blank=False, null=False) - image = models.CharField('Image', max_length=255, blank=False, null=False) - tag = models.CharField('Tag', max_length=255, blank=False, null=False, default='latest') - # Platform-related - arch = models.CharField('Architecture', max_length=36, blank=False, null=False, default='x86_64') - os = models.CharField('Operating system', max_length=36, blank=False, null=False, default='linux') + # Image name + image_name = models.CharField('Image', max_length=255, blank=False, null=False) + + # Image identifiers + image_tag = models.CharField('Tag', max_length=255, blank=True, null=True, default='latest') + image_arch = models.CharField('Architecture', max_length=36, blank=True, null=True) + image_os = models.CharField('Operating system', max_length=36, blank=True, null=True) + # -- OR -- + image_digest = models.CharField('SHA 256 digest', max_length=96, blank=True, null=True) # TODO: do we want more control with respect to kernel, CPUs, instruction sets? # requires = i.e. kernel > 3, intel, AVX2 @@ -142,15 +146,29 @@ class Container(models.Model): def __str__(self): 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)) + return str('Container "{}" of user "{}" with image name "{}" and image tag "{}" on registry "{}" '.format(self.name, user_str, self.image_name, self.image_tag, self.registry)) + + def save(self, *args, **kwargs): + # Check that digest starts with sha256: + if self.image_digest and not self.image_digest.startswith('sha256:'): + raise ValueError('The digest field must start with "sha256:"') + + super(Container, self).save(*args, **kwargs) - @ property + @property + def family_id(self): + return base64.b64encode('{}\t{}\t{}'.format(self.name, self.registry, self.image_name).encode('utf8')).decode('utf8') + + + @property def color(self): - string_int_hash = hash_string_to_int(self.name + self.registry + self.image) + string_int_hash = hash_string_to_int(self.name + self.registry + self.image_name) color_map_index = string_int_hash % len(color_map) return color_map[color_map_index] + + #========================= # Computing resources #========================= 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 c4e167ac1049b59c2329a5fea10cb63748ea6ada..355abf06eb5f93d10d0cbbd8a921860f96bf58f9 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/container.html +++ b/services/webapp/code/rosetta/core_app/templates/components/container.html @@ -2,7 +2,12 @@ {% if details %} - <table class="dashboard" style="margin:10px; max-width:600px"> + <div style="width:400px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:15px"> + <table class="dashboard" style="margin:0px; border:0px; width:398px"> + + <tr> + <td colspan=2 align=center style="padding:10px; font-size:1.2em"><a href="/software/?container_uuid={{ container.uuid }}">{{ container.name }} <font style="font-size:0.9em">({{ container.image_tag }})</font></a></td> + </tr> <tr> <td colspan="2" style="background:{{ container.color }}; height:15px"></td> @@ -10,41 +15,36 @@ <tr><td colspan=2 style="height:5px"></td></tr> - <tr> - <td><b>Name</b></td> - <td>{{ container.name }}</td> + <td colspan=2>{{ container.description }}</td> </tr> + <tr><td colspan=2><hr style="margin:5px"></td></tr> + <tr> <td><b>Owner</b></td> <td>{% if container.user %}{{ container.user }}{% else %}platform{% endif %}</td> </tr> - <tr> - <td><b>Description</b></td> - <td>{{ container.description }}</td> - </tr> - - <tr><td colspan=2><hr style="margin:5px"></td></tr> - <tr> <td><b>Registry</b></td> <td>{{ container.registry }}</td> </tr> <tr> - <td><b>Image</b></td> - <td><code>{{ container.image }}</code></td> + <td><b>Image name</b></td> + <td><code>{{ container.image_name }}</code></td> </tr> <tr> - <td><b>Tag</b></td> - <td>{{ container.tag }}</td> + <td><b>Image tag</b></td> + <td><span class="badge badge-secondary">{{ container.image_tag }}</span></td> </tr> <tr><td colspan=2><hr style="margin:5px"></td></tr> - + </table> + + <table class="dashboard" style="margin:0px; border:0px;"> <tr> <td><b>Interface protocol</b></td> <td>{{ container.interface_protocol }}</td> @@ -61,13 +61,18 @@ </tr> <tr> - <td><b>Architecture</b></td> - <td>{{ container.arch }}</td> + <td><b>Image arch</b></td> + <td>{{ container.image_arch }}</td> </tr> <tr> - <td><b>Operating System</b></td> - <td>{{ container.os }}</td> + <td><b>Image OS</b></td> + <td>{{ container.image_os }}</td> + </tr> + + <tr> + <td><b>Image digest</b></td> + <td>{{ container.image_digest }}</td> </tr> <tr><td colspan=2><hr style="margin:5px"></td></tr> @@ -101,7 +106,7 @@ </tr> {% endif %} </table> - <br/> + </div> {% else %} @@ -109,7 +114,7 @@ <div style="padding:10px; margin-top:5px; text-align:center; border-bottom: {{container.color}} solid 10px; "> - <a href="/software/container_?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; "> @@ -124,8 +129,13 @@ <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>Arch:</b> {{container_arch}} + <b>Image:</b> <code>{{ container.image_name }}</code><br/> + <b>Tag:</b> <span class="badge badge-secondary" style="margin-right:3px">{{ container.image_tag }}</span> + {% if container.image_arch %} + <font style="font-size:0.9em">({{ container.image_arch }})</font> + {% endif %} + + <!-- <b>Arch:</b> {{ container.image_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 index 043523a2b4304e2337a692f1a99fd174c73cbc69..f8e80181b9570a16421fea999758f88d83020dfb 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/container_family.html +++ b/services/webapp/code/rosetta/core_app/templates/components/container_family.html @@ -27,23 +27,60 @@ {% endif %} <div class="image-version-box"> - <b>Image:</b> <code>{{ container_family.image }}</code><br/> + <b>Image:</b> <code>{{ container_family.image_name }}</code><br/> <div style="margin-top:2px"> - <span style="vertical-align:top;"><b>Tag:</b> </span> + <!-- <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> + <option value="{{ container.uuid }}">{{ container.image_tag }} ({{ container.image_arch }})</option> + {% endfor %} + </select> --> + + <!--<span style="vertical-align:top; margin-left:0px"><b>Tag:</b> </span> + <select name="task_container" style="font-size:0.8em"> + {% for arch,container_by_tags in container_family.container_by_tags_by_arch.items %} + {% if not arch %} + {% for tag,container in container_by_tags.items %} + <option value="family:{{ container_family_id }},tag:{{ tag }}">{{ tag }}</option> + {% endfor %} + {% endif %} {% 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,container_by_tags in container_family.container_by_tags_by_arch.items %} + {% if arch %} + <optgroup label="{{ arch }}"> + {% for tag,container in container_by_tags.items %} + <option value="uuid:{{ container.uuid }}">{{ tag }}</option> + {% endfor %} + {% endif %} + {% endfor %} + </optgroup>--> - {% for arch in container_family.all_archs %} - <option value="{{ arch }}">{{ arch }}</option> + + <span style="vertical-align:top; margin-left:0px"><b>Tag:</b> </span> + <select name="task_container_uuid" style="font-size:0.8em"> + {% for arch,container_by_tags in container_family.container_by_tags_by_arch.items %} + {% if not arch %} + {% for tag,container in container_by_tags.items %} + <option value="{{container.uuid}}">{{ tag }}</option> {% endfor %} + {% endif %} + {% endfor %} + + {% for arch,container_by_tags in container_family.container_by_tags_by_arch.items %} + {% if arch %} + <optgroup label="{{ arch }}"> + {% for tag,container in container_by_tags.items %} + <option value="{{container.uuid}}">{{ tag }}</option> + {% endfor %} + {% endif %} + {% endfor %} + </optgroup> + + + + </select> diff --git a/services/webapp/code/rosetta/core_app/templates/software.html b/services/webapp/code/rosetta/core_app/templates/software.html index b5094966c39a8706b89c225b1291705942bcefc2..4b9223bae72d2eac9a76c151044d202db3f0d182 100644 --- a/services/webapp/code/rosetta/core_app/templates/software.html +++ b/services/webapp/code/rosetta/core_app/templates/software.html @@ -9,11 +9,11 @@ <div class="dashboard"> <div class="span8 offset2"> - {% if data.details %} + {% if data.details or data.container %} {% 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> + {% else %} + <h1><a href="/software">Software containers</a> <span style="font-size:18px"> / <a href="/software/?container_family_id={{data.container.family_id}}&details=True">{{ data.container.name }}</a> / {{ data.container.image_tag}}</span></h1> {% endif %} {% else %} {% if data.mode == 'new_task' %} diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py index 1f6c35cf9bb70b92ecf7b9ad2c7195c2e1b3d2d6..bd1bd898c953ffbbf3ab8c86747df66282f03650 100644 --- a/services/webapp/code/rosetta/core_app/views.py +++ b/services/webapp/code/rosetta/core_app/views.py @@ -507,7 +507,6 @@ def new_task(request): # 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)) @@ -519,24 +518,27 @@ def new_task(request): # 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) # Check that container required architecture is compatible with the computing resource - if data['task_container_arch'] != 'auto': - if data['task_container_arch'] != data['task_computing'].arch: + if data['task_container'].image_arch: + if data['task_container'].image_arch != data['task_computing'].arch: # TODO: support setting the container runtime when creating the task # TODO: refactor and unroll this code - container_runtime = data['task_computing'].container_runtimes[0] - if container_runtime in data['task_computing'].emulated_archs and data['task_container_arch'] in data['task_computing'].emulated_archs[container_runtime]: - data['arch_emulation'] = True + # TODO: add support for setting the container runtime + if data['task_computing'].emulated_archs: + container_runtime = data['task_computing'].container_runtimes[0] + if container_runtime in data['task_computing'].emulated_archs and data['task_container'].image_arch in data['task_computing'].emulated_archs[container_runtime]: + data['arch_emulation'] = True + else: + raise ErrorMessage('This computing resource does not support architecture "{}" nor as native or emulated'.format(data['task_container'].image_arch)) else: - raise ErrorMessage('This computing resource does not support architecture "{}" nor as native or emulated'.format(data['task_container_arch'])) + raise ErrorMessage('This computing resource does not support architecture "{}" nor as native or emulated'.format(data['task_container'].image_arch)) else: - raise ErrorMessage('Auto architectures are not supported yet') + raise ErrorMessage('Auto detecting architectures is not supported yet') # Generate random auth token @@ -767,82 +769,92 @@ def software(request): logger.error('Error in getting container with uuid="{}" or performing the required action: "{}"'.format(uuid, e)) return render(request, 'error.html', {'data': data}) - # 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 = [] + else: + # Ddo we have to operate on a container family? + if container_family_id: - # Create all container list - data['containers'] = list(user_containers) + list(platform_containers) + # Get back name, registry and image from contsainer url + container_name, container_registry, container_image_name = 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_name=container_image_name) + platform_containers = Container.objects.filter(user=None, name=container_name, registry=container_registry, image_name=container_image_name) - # Merge containers with the same name, registry and image - data['container_families'] = {} - - # Container family support class - class ContainerFamily(object): + 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_name__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) - 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(','): + # Ok, nilter by owner + if search_owner != 'All': + if search_owner == 'User': + platform_containers =[] + if search_owner == 'Platform': + user_containers = [] - 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 + # Create all container list + data['containers'] = list(user_containers) + list(platform_containers) + + # Merge containers with the same name, registry and image name + data['container_families'] = {} - @ property - def color(self): - try: - return self.members[0].color - except IndexError: - return '#000000' + # Container family support class + class ContainerFamily(object): - # 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) + def __init__(self, id, name, registry, image_name): + self.id = id + self.name = name + self.registry = registry + self.image_name = image_name + 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 + + if not container.image_arch in self.all_archs: + self.all_archs.append(container.image_arch) + + if not container.image_arch in self.container_by_tags_by_arch: + self.container_by_tags_by_arch[container.image_arch]={} + self.container_by_tags_by_arch[container.image_arch][container.image_tag] = container + + # Lastly, add the container to the "all tags" + #if None not in self.container_by_tags_by_arch: + # self.container_by_tags_by_arch[None]={} + #self.container_by_tags_by_arch[None][container.image_tag] = container + + @ property + def color(self): + try: + return self.members[0].color + except IndexError: + return '#000000' + + # Populate container families + for container in data['containers']: + 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_name) + data['container_families'][container.family_id].add(container) + # Finalize the families + #for container.family_id in data['container_families']: + # if len(data['container_families'][container.family_id].all_archs) == 1: + # if data['container_families'][container.family_id].all_archs[0] != None: + # data['container_families'][container.family_id].container_by_tags_by_arch.pop(None) + return render(request, 'software.html', {'data': data}) @@ -869,17 +881,17 @@ def add_software(request): # Container registry container_registry = request.POST.get('container_registry', None) - # Container image - container_image = request.POST.get('container_image',None) + # Container image name + container_image_name = request.POST.get('container_image_name',None) # Container tag - container_tag = request.POST.get('container_tag', None) + container_image_tag = request.POST.get('container_image_tag', None) # Container architecture - container_arch = request.POST.get('container_arch') + container_image_arch = request.POST.get('container_image_arch') # Container operating system - container_os = request.POST.get('container_os') + container_image_os = request.POST.get('container_image_os') # Container interface port container_interface_port = request.POST.get('container_interface_port', None) @@ -921,10 +933,10 @@ def add_software(request): name = container_name, description = container_description, registry = container_registry, - image = container_image, - tag = container_tag, - arch = container_arch, - os = container_os, + image_name = container_image_name, + image_tag = container_image_tag, + image_arch = container_image_arch, + image_os = container_image_os, interface_port = container_interface_port, interface_protocol = container_interface_protocol, interface_transport = container_interface_transport,