diff --git a/services/webapp/code/rosetta/core_app/admin.py b/services/webapp/code/rosetta/core_app/admin.py index b69c3b232dc099e5e241bd162bf974503d6dd1eb..819a338386178021c1b0e3e17075f93740d63d28 100644 --- a/services/webapp/code/rosetta/core_app/admin.py +++ b/services/webapp/code/rosetta/core_app/admin.py @@ -2,10 +2,14 @@ from django.contrib import admin from .models import Profile, LoginToken, Task, Container, Computing, Storage, KeyPair, Page +# Define a extra "ModelAdmin" for the Container model to allow "save as" to easily duplicate containers +class ContainerForAdmin(admin.ModelAdmin): + save_as = True + admin.site.register(Profile) admin.site.register(LoginToken) admin.site.register(Task) -admin.site.register(Container) +admin.site.register(Container, ContainerForAdmin) admin.site.register(Computing) admin.site.register(Storage) admin.site.register(KeyPair) 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 b5ffae06cc56e4441f6b905413c9a933ed8b7b41..b3ac3be5cd48406153cfe001228d9e9be818a0fb 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 @@ -129,15 +129,15 @@ to provide help, news and informations on your deployment. Or you can just ignor # Minimal Desktop Container.objects.create(user = None, - name = 'Minimal Desktop ', + 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 = 'x86_64', - 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,43 +147,76 @@ 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 = 'x86_64', - 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, interface_auth_user = None) - # Jupyter Notebook + # Jupyter Notebook Container.objects.create(user = None, name = 'Jupyter Notebook', description = 'A Jupyter Notebook server', registry = 'docker.io', - image = 'sarusso/jupyternotebook', - tag = 'v0.2.0', - arch = 'x86_64', - 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 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 Container.objects.create(user = None, name = 'SSH server', description = 'An SSH server supporting X forwarding as well.', registry = 'docker.io', - image = 'sarusso/ssh', - tag = 'v0.2.0', - arch = 'x86_64', - 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', @@ -232,7 +265,7 @@ to provide help, news and informations on your deployment. Or you can just ignor access_mode = 'internal', auth_mode = 'internal', wms = None, - container_runtimes = 'docker') + container_runtimes = ['docker']) # Demo standalone computing plus conf @@ -243,7 +276,7 @@ to provide help, news and informations on your deployment. Or you can just ignor auth_mode = 'user_keys', wms = None, conf = {'host': 'slurmclusterworker-one'}, - container_runtimes = 'singularity') + container_runtimes = ['singularity']) # Add testuser extra conf for this computing resource testuser.profile.add_extra_conf(conf_type = 'computing_user', object=demo_singlenode_computing, value= 'slurmtestuser') @@ -256,7 +289,7 @@ to provide help, news and informations on your deployment. Or you can just ignor auth_mode = 'user_keys', wms = 'slurm', conf = {'host': 'slurmclustermaster-main', 'default_partition': 'partition1'}, - container_runtimes = 'singularity') + container_runtimes = ['singularity']) # Add testuser extra conf for this computing resource testuser.profile.add_extra_conf(conf_type = 'computing_user', object=demo_slurm_computing, value= 'slurmtestuser') diff --git a/services/webapp/code/rosetta/core_app/migrations/0022_auto_20211121_1341.py b/services/webapp/code/rosetta/core_app/migrations/0022_auto_20211121_1341.py new file mode 100644 index 0000000000000000000000000000000000000000..a7de65727fd20083d6f0ae1f9121684a512a81cd --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0022_auto_20211121_1341.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2021-11-21 13:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0021_container_interface_auth_user'), + ] + + operations = [ + migrations.RenameField( + model_name='computing', + old_name='container_runtimes', + new_name='container_runtime', + ), + ] diff --git a/services/webapp/code/rosetta/core_app/migrations/0023_auto_20211121_1342.py b/services/webapp/code/rosetta/core_app/migrations/0023_auto_20211121_1342.py new file mode 100644 index 0000000000000000000000000000000000000000..dcde2e0c7b6059129e10bed3cd7d9c7de556481a --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0023_auto_20211121_1342.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.1 on 2021-11-21 13:42 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0022_auto_20211121_1341'), + ] + + operations = [ + migrations.RemoveField( + model_name='computing', + name='container_runtime', + ), + migrations.AddField( + model_name='computing', + name='container_runtimes', + field=django.contrib.postgres.fields.jsonb.JSONField(default=['docker'], verbose_name='Container runtimes'), + preserve_default=False, + ), + ] diff --git a/services/webapp/code/rosetta/core_app/migrations/0024_computing_emulated_archs.py b/services/webapp/code/rosetta/core_app/migrations/0024_computing_emulated_archs.py new file mode 100644 index 0000000000000000000000000000000000000000..0add854a817e3e9de8d0bec98ffb379c5cd4af18 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0024_computing_emulated_archs.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2021-11-21 13:42 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0023_auto_20211121_1342'), + ] + + operations = [ + migrations.AddField( + model_name='computing', + name='emulated_archs', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Emulated architectures'), + ), + ] 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/migrations/0027_computing_supported_archs.py b/services/webapp/code/rosetta/core_app/migrations/0027_computing_supported_archs.py new file mode 100644 index 0000000000000000000000000000000000000000..cb6ee53a3d36f7ea3e8270d5c013d2e7d120bbb2 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0027_computing_supported_archs.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2021-11-24 00:25 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0026_auto_20211123_0037'), + ] + + operations = [ + migrations.AddField( + model_name='computing', + name='supported_archs', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Supported architectures'), + ), + ] diff --git a/services/webapp/code/rosetta/core_app/migrations/0028_computing_arch.py b/services/webapp/code/rosetta/core_app/migrations/0028_computing_arch.py new file mode 100644 index 0000000000000000000000000000000000000000..7088e4c1b66f07a73e70a25514cf992ed8f865c8 --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0028_computing_arch.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2021-11-24 01:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0027_computing_supported_archs'), + ] + + operations = [ + migrations.AddField( + model_name='computing', + name='arch', + field=models.CharField(default='amd64', max_length=255, verbose_name='Architecture'), + preserve_default=False, + ), + ] diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 77514a195336d0f778c95a0318ea50c6e6f4c07b..e772eea43853f599a29c23658e436312d2a2fcb6 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=False, null=False, 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,16 +146,30 @@ 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): + id_as_str = '{}\t{}\t{}'.format(self.name, self.registry, self.image_name) + id_as_base64_str = base64.b64encode(id_as_str.encode('utf8')).decode('utf8') + return id_as_base64_str + + @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_name) color_map_index = string_int_hash % len(color_map) return color_map[color_map_index] + + #========================= # Computing resources #========================= @@ -165,16 +183,24 @@ class Computing(models.Model): name = models.CharField('Name', max_length=255, blank=False, null=False) description = models.TextField('Description', blank=True, null=True) - # Tye (standalone / cluster) + # Type (standalone / cluster) and arch (i.e. amd64) type = models.CharField('Type', max_length=255, blank=False, null=False) + arch = models.CharField('Architecture', max_length=255, blank=False, null=False) # Interfce and interaction definition access_mode = models.CharField('Access (control) mode', max_length=36, blank=False, null=False) auth_mode = models.CharField('Auth mode', max_length=36, blank=False, null=False) wms = models.CharField('Workload management system', max_length=36, blank=True, null=True) - # Supported container runtimes - container_runtimes = models.CharField('Container runtimes', max_length=256, blank=False, null=False) + # Supported container runtimes ['docker', 'singularity'] + container_runtimes = JSONField('Container runtimes', blank=False, null=False) + #container_runtime = models.CharField('Container runtimes', max_length=256, blank=False, null=False) + + # Supported architectures (i.e. 386 for amd64), as list: ['386'] + supported_archs = JSONField('Supported architectures', blank=True, null=True) + + # Emulated architectures, by container runtime: {'docker': ['arm64/v7', 'arm64/v8'] + emulated_archs = JSONField('Emulated architectures', blank=True, null=True) # Conf conf = JSONField(blank=True, null=True) @@ -203,7 +229,7 @@ class Computing(models.Model): @property def default_container_runtime(self): return str(self.container_runtimes).split(',')[0] - + #======================= # Computing manager diff --git a/services/webapp/code/rosetta/core_app/templates/add_software.html b/services/webapp/code/rosetta/core_app/templates/add_software.html index 668480db5ab5e5ec987492fe8ca9f9a5496bea49..9487623e74a2b1b1f77e143603e2000732c25770 100644 --- a/services/webapp/code/rosetta/core_app/templates/add_software.html +++ b/services/webapp/code/rosetta/core_app/templates/add_software.html @@ -44,16 +44,16 @@ </tr> <tr> - <td><b>Image</b></td> + <td><b>Image name</b></td> <td> - <input type="text" name="container_image" value="" placeholder="" size="23" required /> + <input type="text" name="container_image_name" value="" placeholder="" size="23" required /> </td> </tr> <tr> - <td><b>Tag</b></td> + <td><b>Image tag</b></td> <td> - <input type="text" name="container_tag" value="latest" size="23" required /> + <input type="text" name="container_image_tag" value="latest" size="23" required /> </td> </tr> @@ -77,7 +77,7 @@ {% else %} <select name="container_interface_protocol" > <option value="http" selected>http</option> - <option value="https" selected>https</option> + <option value="https">https</option> </select> {% endif %} </td> @@ -94,21 +94,25 @@ <table class="dashboard" style="width:360px; margin-bottom:25px"> <tr> - <td><b>Architecture</b></td><td> - <select name="container_arch" > - <option value="x86_64" selected>x86_64</option> - </select> + <td><b>Image arch</b></td><td> + <input type="text" name="container_image_arch" value="" placeholder="" size="5" /> </td> </tr> <tr> - <td><b>Operating System</b></td><td> - <select name="container_os" > + <td><b>Image OS</b></td><td> + <select name="container_image_os" > <option value="linux" selected>linux</option> </select> </td> </tr> + <tr> + <td><b>Image digest</b></td><td> + <input type="text" name="container_image_digest" value="" placeholder="sha256:..." size="15" /> + </td> + </tr> + <tr> <td><b>Interface transport</b></td><td> <select name="container_interface_transport" > @@ -154,7 +158,7 @@ {% else %} - Ok, Container added. Go back to your <a href="/containers">container list</a>. + Ok, software container added. Go back to <a href="/software">software</a>. {% endif %} 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..0ad13b5d71d7090ba89a27cadec6fd23690986b1 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/computing.html +++ b/services/webapp/code/rosetta/core_app/templates/components/computing.html @@ -23,7 +23,7 @@ <td><b>Type</b></td> <td>{{ data.computing.type }}</td> </tr> - + <tr> <td><b>Access mode</b></td> <td>{{ data.computing.access_mode }}</td> @@ -39,11 +39,20 @@ <td>{{ data.computing.wms }}</td> </tr> + <tr> + <td><b>Arch</b></td> + <td>{{ data.computing.arch }}</td> + </tr> + <tr> <td><b>Container runtimes</b></td> <td>{{ data.computing.container_runtimes }}</td> </tr> + <tr> + <td><b>Emulated archs</b></td> + <td>{{ data.computing.emulated_archs }}</td> + </tr> <tr> <td><b>Storages</b></td> @@ -85,11 +94,7 @@ {% endif %} <div class="image-version-box"> <b>Type:</b> {{ computing.type }}<br/> - <!-- -->{% if computing.auth_mode == 'user_keys' %} - <b>Access:</b> user keys - {% else %} - <b>Access:</b> open - {% endif %} + <b>Arch:</b> {{ computing.arch }} <br/> <b>Storages:</b> {% if not computing.storages.all %} @@ -115,6 +120,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..49a60652d435e4763dde73d6ce08664012acb593 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/container.html +++ b/services/webapp/code/rosetta/core_app/templates/components/container.html @@ -1,8 +1,13 @@ - {% if details %} + {% 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,24 @@ </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> + {% if container.image_digest %} + <input type="text" style="margin:0 "size=25 class="form-control" placeholder="" value="{{ container.image_digest }}" name='digest' readonly> + {% else %} + None + {% endif %} + </td> </tr> <tr><td colspan=2><hr style="margin:5px"></td></tr> @@ -101,7 +112,7 @@ </tr> {% endif %} </table> - <br/> + </div> {% else %} @@ -109,7 +120,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; "> @@ -124,8 +135,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>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 new file mode 100644 index 0000000000000000000000000000000000000000..f8e80181b9570a16421fea999758f88d83020dfb --- /dev/null +++ b/services/webapp/code/rosetta/core_app/templates/components/container_family.html @@ -0,0 +1,115 @@ + + {% 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_name }}</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.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 %} + + {% 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>--> + + + <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> + + + </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/components/task.html b/services/webapp/code/rosetta/core_app/templates/components/task.html index 35ef478837dccaf21412545169f924017b490827..effe0f1ccb5a21bf3e92b3ae3cc1456546993aa0 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/task.html +++ b/services/webapp/code/rosetta/core_app/templates/components/task.html @@ -5,6 +5,8 @@ <div style="margin-top:5px; padding:10px; text-align:center; border-bottom: #f8f8f8 solid 1px; "> Task <b>{{data.task.name}}</b> summary </div> + <hr style="margin:0"> + {% else %} <div style="width:350px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:20px"> @@ -27,18 +29,18 @@ </div> --> - <div style="padding:10px;"> + <div style="padding:10px; padding-left:15px; padding-right:15px; text-align:left;"> <!-- <b>Container:</b> {{ task.container.name }} <span style="font-size:14px; background-color:{{task.container.color}}"> </span> <br/> <b>Computing:</b> {{ task.computing.name }} <span style="font-size:14px; background-color:{{task.computing.color}}"> </span><br/> --> - <b>Software:</b> - <a href="/software/?uuid={{ task.container.uuid }}" style="color:{{task.container.color}}">{{ task.container.name }}</a> + <b>Software container:</b> + <a href="/software/?container_uuid={{ task.container.uuid }}" style="color:{{task.container.color}}">{{ task.container.name }}</a> {% if task.container.type == 'docker' %}<img src="/static/img/docker-logo.svg" style="height:18px; width:18px; margin-bottom:4px" />{% endif %} {% if task.container.type == 'singularity' %}<img src="/static/img/singularity-logo.svg" style="height:18px; width:18px; margin-bottom:4px" />{% endif %} <br/> - <b>Computing:</b> <a href="/computing/?uuid={{ task.computing.uuid }}">{{ task.computing.name }}</a> + <b>Computing resource:</b> <a href="/computing/?uuid={{ task.computing.uuid }}">{{ task.computing.name }}</a> <!-- <a href="/computing/?uuid={{ task.computing.uuid }}" no_style="color:{{task.computing.color}}"><i class="fa fa-external-link" ></i></a><br/> --> <div style="margin-top:2px"> 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..807e554010b057c255154ef7cf15a7d243bf77fd 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 }}" /> @@ -165,6 +166,18 @@ <i class="fa fa-exclamation-triangle" style="color:orange"></i> This container does not support custom interface ports and the computing resource you selected might use a container runtime which does not support port mapping (Singularity). In this case, if the container interface port is already allocated, the task will fail to start. </p></div> {% endif %} + + {% if data.arch_emulation %} + <div> <p style="font-size:15px; max-width:700px; margin-bottom:20px; margin-left:5px"> + <i class="fa fa-exclamation-triangle" style="color:orange"></i> The selected software container architecture ({{ data.task_container_arch}}) can only be emulated on this computing resource, meaning the it will probably run quite slow. + </p></div> + {% endif %} + + {% if data.arch_auto_selection %} + <div> <p style="font-size:15px; max-width:700px; margin-bottom:20px; margin-left:5px"> + <i class="fa fa-exclamation-triangle" style="color:orange"></i> The selected software container does not specify any architecture. This will leave to the container runtime either to auto-select the right image architecture on the registry, or to fallback on emulation if not found. Beware of potential incompatibilities or slowdowns. + </p></div> + {% endif %} <!-- {% if data.task_container.interface_port and not data.task_container.supports_interface_auth %} <div> <p style="font-size:15px; max-width:700px; margin-bottom:20px; margin-left:5px"> diff --git a/services/webapp/code/rosetta/core_app/templates/software.html b/services/webapp/code/rosetta/core_app/templates/software.html index 8be03d0b5deed876bd26394f2d26f618dc23ec76..4b9223bae72d2eac9a76c151044d202db3f0d182 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 %} - <h1><a href="/software">Software containers</a> <span style="font-size:18px"> / {{ data.container.name }}</span></h1> + {% 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"> / <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' %} <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..2e75f1ae5884da4fd367e0c1a4c3c58518b8c695 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 @@ -496,7 +497,7 @@ def new_task(request): elif step == 'two': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) # List all computing resources @@ -507,12 +508,34 @@ def new_task(request): elif step == 'three': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) # Get computing resource data['task_computing'] = get_task_computing(request) + # Check that container required architecture is compatible with the computing resource + # TODO: support setting the container runtime when creating the task + # TODO: refactor and unroll this code + if data['task_container'].image_arch: + if (data['task_container'].image_arch != data['task_computing'].arch) and (data['task_container'].image_arch not in data['task_computing'].supported_archs): + + 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'].image_arch)) + + else: + data['arch_auto_selection'] = True + #raise ErrorMessage('Auto selecting architectures is not supported yet') + # Generate random auth token data['task_auth_token'] = str(uuid.uuid4()) @@ -523,7 +546,7 @@ def new_task(request): elif step == 'last': - # Get software container + # Get software container and arch data['task_container'] = get_task_container(request) # Get computing resource @@ -691,8 +714,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 +727,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 +735,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 +762,93 @@ 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) + # Ddo we have to operate on a container family? + if container_family_id: + + # 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) + + 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) - # Filter by owner - if search_owner != 'All': - if search_owner == 'User': - platform_containers =[] - if search_owner == 'Platform': - user_containers = [] - - data['containers'] = list(user_containers) + list(platform_containers) - + + # 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 name + data['container_families'] = {} + + # Container family support class + class ContainerFamily(object): + + 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}) @@ -781,17 +875,20 @@ 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 + container_image_tag = request.POST.get('container_image_tag', None) + + # Container image architecture + container_image_arch = request.POST.get('container_image_arch', None) - # Container architecture - container_arch = request.POST.get('container_arch') + # Container image OS + container_image_os = request.POST.get('container_image_os', None) - # Container operating system - container_os = request.POST.get('container_os') + # Container image digest + container_image_digest = request.POST.get('container_image_digest', None) # Container interface port container_interface_port = request.POST.get('container_interface_port', None) @@ -829,14 +926,15 @@ def add_software(request): #logger.debug('Creating new container object with image="{}", type="{}", registry="{}", ports="{}"'.format(container_image, container_type, container_registry, container_ports)) # Create - Container.objects.create(user = request.user, - name = container_name, - description = container_description, - registry = container_registry, - image = container_image, - tag = container_tag, - arch = container_arch, - os = container_os, + Container.objects.create(user = request.user, + name = container_name, + description = container_description, + registry = container_registry, + image_name = container_image_name, + image_tag = container_image_tag, + image_arch = container_image_arch, + image_os = container_image_os, + image_digest = container_image_digest, interface_port = container_interface_port, interface_protocol = container_interface_protocol, interface_transport = container_interface_transport,