From 3b9f8748f274a8380e1d060e1745d8442903f1b2 Mon Sep 17 00:00:00 2001 From: Stefano Alberto Russo <stefano.russo@gmail.com> Date: Fri, 16 Aug 2024 13:37:48 +0200 Subject: [PATCH] Code cleanup (removed unecessary spaces). --- rosetta/build | 14 +- rosetta/logs | 2 +- rosetta/shell | 2 +- rosetta/status | 10 +- services/webapp/code/rosetta/auth.py | 11 +- .../webapp/code/rosetta/context_processors.py | 8 +- services/webapp/code/rosetta/core_app/api.py | 451 +++++++++--------- .../rosetta/core_app/computing_managers.py | 161 +++---- .../code/rosetta/core_app/decorators.py | 9 +- .../code/rosetta/core_app/exceptions.py | 2 +- .../webapp/code/rosetta/core_app/fields.py | 2 +- .../management/commands/core_app_populate.py | 73 +-- .../webapp/code/rosetta/core_app/models.py | 121 ++--- .../rosetta/core_app/templates/account.html | 51 +- .../core_app/templates/add_profile_conf.html | 27 +- .../core_app/templates/add_software.html | 75 +-- .../rosetta/core_app/templates/backend.html | 13 +- .../templates/components/computing.html | 30 +- .../templates/components/container.html | 64 +-- .../components/container_family.html | 46 +- .../core_app/templates/components/task.html | 76 +-- .../templates/components/tzselect.html | 14 +- .../rosetta/core_app/templates/computing.html | 15 +- .../rosetta/core_app/templates/error.html | 9 +- .../rosetta/core_app/templates/footer.html | 9 +- .../rosetta/core_app/templates/header.html | 8 +- .../core_app/templates/import_repository.html | 39 +- .../rosetta/core_app/templates/login.html | 25 +- .../code/rosetta/core_app/templates/main.html | 9 +- .../core_app/templates/navigation.html | 45 +- .../rosetta/core_app/templates/new_task.html | 65 +-- .../code/rosetta/core_app/templates/page.html | 5 +- .../rosetta/core_app/templates/register.html | 27 +- .../rosetta/core_app/templates/software.html | 57 +-- .../rosetta/core_app/templates/storage.html | 9 +- .../rosetta/core_app/templates/success.html | 13 +- .../core_app/templates/task_connect.html | 49 +- .../rosetta/core_app/templates/task_log.html | 27 +- .../rosetta/core_app/templates/tasks.html | 13 +- .../code/rosetta/core_app/tests/common.py | 11 +- .../code/rosetta/core_app/tests/test_apis.py | 10 +- .../rosetta/core_app/tests/test_models.py | 13 +- .../code/rosetta/core_app/tests/test_utils.py | 21 +- .../webapp/code/rosetta/core_app/utils.py | 201 ++++---- .../webapp/code/rosetta/core_app/views.py | 287 +++++------ services/webapp/code/rosetta/settings.py | 25 +- services/webapp/code/rosetta/urls.py | 25 +- services/webapp/prestartup_webapp.sh | 2 - services/webapp/run_webapp.sh | 21 +- 49 files changed, 1158 insertions(+), 1144 deletions(-) diff --git a/rosetta/build b/rosetta/build index 661699a..a3238b1 100755 --- a/rosetta/build +++ b/rosetta/build @@ -12,7 +12,7 @@ if [[ "x$1" == "xnocache" ]] ; then NOCACHE=true SERVICE="" elif [[ "x$2" == "xnocache" ]] ; then - NOCACHE=true + NOCACHE=true SERVICE=$1 else if [[ "x$NOCACHE" == "x" ]] ; then @@ -29,20 +29,20 @@ else fi if [[ "x$SERVICE" == "x" ]] ; then - + # Build all services NOCACHE=$NOCACHE rosetta/build base_ubuntu18.04 NOCACHE=$NOCACHE rosetta/build base_ubuntu22.04 NOCACHE=$NOCACHE rosetta/build slurmbase - NOCACHE=$NOCACHE rosetta/build slurmcluster - NOCACHE=$NOCACHE rosetta/build slurmclustermaster - NOCACHE=$NOCACHE rosetta/build slurmclusterworker - NOCACHE=$NOCACHE rosetta/build standaloneworker + NOCACHE=$NOCACHE rosetta/build slurmcluster + NOCACHE=$NOCACHE rosetta/build slurmclustermaster + NOCACHE=$NOCACHE rosetta/build slurmclusterworker + NOCACHE=$NOCACHE rosetta/build standaloneworker NOCACHE=$NOCACHE rosetta/build dregistry NOCACHE=$NOCACHE rosetta/build webapp NOCACHE=$NOCACHE rosetta/build postgres NOCACHE=$NOCACHE rosetta/build proxy - + else # Build a specific image diff --git a/rosetta/logs b/rosetta/logs index c1f7123..844646e 100755 --- a/rosetta/logs +++ b/rosetta/logs @@ -14,7 +14,7 @@ fi if [[ "x$2" != "x" ]] ; then tail -f -n 1000 data/$1/log/$2.log else - docker-compose logs -f $1 + docker-compose logs -f $1 fi diff --git a/rosetta/shell b/rosetta/shell index 1cf6033..03580d4 100755 --- a/rosetta/shell +++ b/rosetta/shell @@ -14,7 +14,7 @@ elif [[ $# -gt 2 ]] ; then echo "Use double quotes to wrap commands with spaces" exit 1 else - + COMMAND=$2 if [[ "x$COMMAND" == "x" ]] ; then echo "" diff --git a/rosetta/status b/rosetta/status index 57a7260..ca1dccd 100755 --- a/rosetta/status +++ b/rosetta/status @@ -10,16 +10,16 @@ if [[ $# -eq 0 ]] ; then declare -a container_names OUT=$(rosetta/ps) - + while read -r line; do - + if [[ $line == *"Up"* ]]; then container_name=$(echo $line | cut -d ' ' -f1) container_names+=($container_name); fi - + done <<< "$OUT" - + for container_name in ${container_names[@]} do echo "" @@ -27,7 +27,7 @@ if [[ $# -eq 0 ]] ; then docker-compose exec $container_name /bin/bash -c "supervisorctl status" done echo "" - + else docker-compose exec $@ /bin/bash -c "supervisorctl status" fi diff --git a/services/webapp/code/rosetta/auth.py b/services/webapp/code/rosetta/auth.py index 92c25a2..b6bc7fb 100644 --- a/services/webapp/code/rosetta/auth.py +++ b/services/webapp/code/rosetta/auth.py @@ -9,9 +9,9 @@ logger = logging.getLogger(__name__) class RosettaOIDCAuthenticationBackend(OIDCAuthenticationBackend): - + def create_user(self, claims): - + # Call parent user creation function user = super(RosettaOIDCAuthenticationBackend, self).create_user(claims) @@ -27,12 +27,12 @@ class RosettaOIDCAuthenticationBackend(OIDCAuthenticationBackend): class RosettaOIDCAuthenticationCallbackView(OIDCAuthenticationCallbackView): - + def login_success(self): - + # Call parent login_success but do not return super(RosettaOIDCAuthenticationCallbackView, self).login_success() - + logger.debug('Trying to get cookie-based post login redirect') post_login_page = self.request.COOKIES.get('post_login_redirect') if post_login_page: @@ -44,3 +44,4 @@ class RosettaOIDCAuthenticationCallbackView(OIDCAuthenticationCallbackView): logger.debug('No cookie-based post login redirect found, redirecting to "%s"', self.success_url) return HttpResponseRedirect(self.success_url) + diff --git a/services/webapp/code/rosetta/context_processors.py b/services/webapp/code/rosetta/context_processors.py index 0d563a3..bf226ab 100644 --- a/services/webapp/code/rosetta/context_processors.py +++ b/services/webapp/code/rosetta/context_processors.py @@ -2,7 +2,7 @@ import os from django.conf import settings def export_vars(request): data = {} - + # Set open id connect enabled or not if settings.OIDC_RP_CLIENT_ID: data['OPENID_ENABLED'] = True @@ -19,6 +19,6 @@ def export_vars(request): if settings.INVITATION_CODE: data['INVITATION_CODE_ENABLED'] = True else: - data['INVITATION_CODE_ENABLED'] = False - - return data \ No newline at end of file + data['INVITATION_CODE_ENABLED'] = False + + return data diff --git a/services/webapp/code/rosetta/core_app/api.py b/services/webapp/code/rosetta/core_app/api.py index 96acb30..bc8886a 100644 --- a/services/webapp/code/rosetta/core_app/api.py +++ b/services/webapp/code/rosetta/core_app/api.py @@ -15,7 +15,7 @@ from .utils import format_exception, send_email, os_shell, now_t, get_ssh_access from .models import Profile, Task, TaskStatuses, Computing, Storage, KeyPair from .exceptions import ConsistencyException import json - + # Setup logging logger = logging.getLogger(__name__) @@ -24,33 +24,33 @@ ROSETTA_AGENT_CHECK_SSL = booleanize(os.environ.get('ROSETTA_AGENT_CHECK_SSL', T #============================== # Common returns #============================== - + # Ok (with data) def ok200(data=None): return Response({"results": data}, status=status.HTTP_200_OK) - + # Error 400 def error400(data=None): return Response({"detail": data}, status=status.HTTP_400_BAD_REQUEST) - + # Error 401 def error401(data=None): return Response({"detail": data}, status=status.HTTP_401_UNAUTHORIZED) - + # Error 404 def error404(data=None): return Response({"detail": data}, status=status.HTTP_404_NOT_FOUND) - + # Error 500 def error500(data=None): return Response({"detail": data}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + #============================== # Authentication helper #============================== - + def rosetta_authenticate(request): # Get data @@ -65,22 +65,22 @@ def rosetta_authenticate(request): # Try username/password authentication elif username or password: - + # Check we got both if not username: return error400('Got empty username') if not password: return error400('Got empty password') - + # Authenticate user = authenticate(username=username, password=password) if not user: - return error401('Wrong username/password') + return error401('Wrong username/password') else: login(request, user) return user - # Try auth toekn authentication + # Try auth toekn authentication elif authtoken: try: profile = Profile.objects.get(authtoken=authtoken) @@ -96,7 +96,7 @@ def rosetta_authenticate(request): # CSRF exempt auth class #============================== -from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from rest_framework.authentication import SessionAuthentication, BasicAuthentication class CsrfExemptSessionAuthentication(SessionAuthentication): @@ -107,10 +107,10 @@ class CsrfExemptSessionAuthentication(SessionAuthentication): #============================== # Base public API class #============================== - + class PublicPOSTAPI(APIView): '''Base public POST API class''' - + # POST def post(self, request): try: @@ -118,9 +118,9 @@ class PublicPOSTAPI(APIView): except Exception as e: logger.error(format_exception(e)) return error500('Got error in processing request: {}'.format(e)) - + class PublicGETAPI(APIView): - '''Base public GET API class''' + '''Base public GET API class''' # GET def get(self, request): try: @@ -134,43 +134,43 @@ class PublicGETAPI(APIView): #============================== # Base private API class #============================== - + class PrivatePOSTAPI(APIView): '''Base private POST API class''' - + # POST def post(self, request): try: # Authenticate using rosetta authentication response = rosetta_authenticate(request) - + # If we got a response return it, otherwise set it as the user. if isinstance(response, Response): return response else: self.user = response - + # Call API logic return self._post(request) except Exception as e: logger.error(format_exception(e)) return error500('Got error in processing request: {}'.format(e)) - + class PrivateGETAPI(APIView): '''Base private GET API class''' - # GET + # GET def get(self, request): try: # Authenticate using rosetta authentication response = rosetta_authenticate(request) - + # If we got a response return it, otherwise set it as the user. if isinstance(response, Response): return response else: self.user = response - + # Call API logic return self._get(request) except Exception as e: @@ -191,29 +191,29 @@ class login_api(PrivateGETAPI, PrivatePOSTAPI): post: Authorize and returns the auth token. """ - + def _post(self, request): return ok200({'authtoken': self.user.profile.authtoken}) def _get(self, request): - return ok200({'authtoken': self.user.profile.authtoken}) - - + return ok200({'authtoken': self.user.profile.authtoken}) + + class logout_api(PrivateGETAPI): """ get: Logout the user """ - + def _get(self, request): logout(request) return ok200() class agent_api(PublicGETAPI): - + def _get(self, request): - + task_uuid = request.GET.get('task_uuid', None) if not task_uuid: return HttpResponse('MISSING task_uuid') @@ -228,9 +228,9 @@ class agent_api(PublicGETAPI): from.utils import get_webapp_conn_string webapp_conn_string = get_webapp_conn_string() - + action = request.GET.get('action', None) - + if not action: # Return the agent code agent_code=''' @@ -290,7 +290,7 @@ if not check_ssl: else: response = urlopen("'''+webapp_conn_string+'''/api/v1/base/agent/?task_uuid={}&action=set_ip_port&ip={}&port={}".format(task_uuid, ip, port)) -response_content = response.read() +response_content = response.read() if response_content not in ['OK', b'OK']: logger.error(response_content) @@ -301,37 +301,37 @@ else: logger.info('Everything OK') print(port) ''' - + return HttpResponse(agent_code) elif action=='set_ip_port': - + task_interface_ip = request.GET.get('ip', None) if not task_interface_ip: return HttpResponse('IP not valid (got "{}")'.format(task_interface_ip)) - + task_interface_port = request.GET.get('port', None) if not task_interface_port: return HttpResponse('Port not valid (got "{}")'.format(task_interface_port)) - + try: int(task_interface_port) except (TypeError, ValueError): return HttpResponse('Port not valid (got "{}")'.format(task_interface_port)) - + # Set fields logger.info('Agent API setting task "{}" to ip "{}" and port "{}"'.format(task.uuid, task_interface_ip, task_interface_port)) task.status = TaskStatuses.running task.interface_ip = task_interface_ip - + # Get container engine container_engine = None if task.computing_options: container_engine = task.computing_options.get('container_engine', None) if not container_engine: container_engine = task.computing.default_container_engine - + if container_engine=='singularity': # For Singularity, set this only if the container supports custom # interface ports. Otherwise, use the task container interface port. @@ -342,10 +342,10 @@ print(port) else: # For all other container engines, set it in any case task.interface_port = int(task_interface_port) - + # Save the task task.save() - + # Notify the user that the task called back home if using a WMS if task.computing.wms: if settings.DJANGO_EMAIL_APIKEY: @@ -357,7 +357,7 @@ print(port) except Exception as e: logger.error('Cannot send task ready email: "{}"'.format(e)) return HttpResponse('OK') - + else: return HttpResponse('Unknown action "{}"'.format(action)) @@ -375,7 +375,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): post: Perform actions or upload files. """ - + # The RichFilemanager has no CSRF support... authentication_classes = (CsrfExemptSessionAuthentication, BasicAuthentication) @@ -384,12 +384,12 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Prepare paths for scp. They have been already made shell-ready, but we need to triple-escape # spaces on remote source or destination: My\ Folder must become My\\\ Folder. - + if mode=='get': source = source.replace('\ ', '\\\\\\ ') else: dest = dest.replace('\ ', '\\\\\\ ') - + # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(computing, user) @@ -410,15 +410,15 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): if storage.access_through_computing: # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(storage.computing, user) - + # Command command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} "{}"'.format(computing_port, computing_keys.private_key_file, computing_user, computing_host, command) else: - raise NotImplementedError('Not accessing through computing is not implemented for storage type "{}"'.format(storage.type)) + raise NotImplementedError('Not accessing through computing is not implemented for storage type "{}"'.format(storage.type)) elif storage.access_mode == 'cli': try: as_user = storage.conf['as_user'] - + # Is "as_user" a UID? try: uid = int(as_user) @@ -440,8 +440,8 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): raise Exception(out.sterr) as_user = 'user_' + str(uid) else: - as_user = out.stdout.strip() - + as_user = out.stdout.strip() + except (KeyError, TypeError): as_user = None if as_user: @@ -449,8 +449,8 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): else: command = '/bin/bash -c "{}"'.format(command) else: - raise NotImplementedError('Access mode "{}" not implemented for storage type "{}"'.format(storage.access_mode, storage.type)) - + raise NotImplementedError('Access mode "{}" not implemented for storage type "{}"'.format(storage.access_mode, storage.type)) + return command @staticmethod @@ -468,14 +468,14 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): @staticmethod def sanitize_and_prepare_shell_path(path, user, storage, escapes=True): - + if escapes: path = path.replace(' ', '\ ') cleaner = re.compile('(?:\\\)+') path = re.sub(cleaner,r"\\",path) - + # Prepare the base path (expand it with variables substitution) - base_path_expanded = storage.base_path + base_path_expanded = storage.base_path if '$SSH_USER' in base_path_expanded: if storage.access_through_computing: computing = storage.computing @@ -486,7 +486,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): base_path_expanded = base_path_expanded.replace('$SSH_USER', computing_user) else: base_path_expanded = base_path_expanded.replace('$SSH_USER', computing.conf.get('user')) - + else: raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in base_path_expanded: @@ -495,7 +495,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # If the path is not starting with the base path, do it if not path.startswith(base_path_expanded): path = base_path_expanded+'/'+path - + return path def get_storage_from_path(self, path, request): @@ -508,10 +508,10 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): except IndexError: storage_name = storage_id computing_name = None - + # Get all the storages this user has access to: storages = list(Storage.objects.filter(group=None, name=storage_name)) + list(Storage.objects.filter(group__user=request.user, name=storage_name)) - + # Filter by computing resource name (or None) if computing_name: unfiltered_storages = storages @@ -524,8 +524,8 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): storages = [] for storage in unfiltered_storages: if storage.computing is None: - storages.append(storage) - + storages.append(storage) + # Check that we had at least and no more than one storage in the end if len(storages) == 0: @@ -537,21 +537,21 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): storage = storages[0] return storage - + def ls(self, path, user, storage): - - # Data container + + # Data container data = [] - + if storage.type == 'generic_posix': shell_path = self.sanitize_and_prepare_shell_path(path, user, storage) - + # Prepare command # https://askubuntu.com/questions/1116634/ls-command-show-time-only-in-iso-format # https://www.howtogeek.com/451022/how-to-use-the-stat-command-on-linux/ - + command = self.prepare_sh_command('cd {} && stat --printf=\'%F/%s/%Y/%n\\n\' * .*'.format(shell_path), user, storage) # Execute_command @@ -560,39 +560,39 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Did we just get a "cannot stat - No such file or directory" (bash) or a "can't cd to" (sh) error? if 'No such file or directory' in out.stderr or 'can\'t cd to' in out.stderr : - + # Create the folder if this was the root for the user (storage base path) # Note: if the folder is completely empty, this gets execute as well. # TODO: fix me (e.g. check for "cannot stat" or similar) if path == '/': self.mkdir(self.sanitize_and_prepare_shell_path('/', user, storage), user, storage, force=True) - + # Return (empty) data return data - + else: raise Exception(out.stderr) - - # Log + + # Log #logger.debug('Shell exec output: "{}"'.format(out)) - + out_lines = out.stdout.split('\n') - + for line in out_lines: - + # Example line: directory/My folder/68/1617030350 - + # Set name line_pieces = line.split('/') type = line_pieces[0] size = line_pieces[1] timestamp = line_pieces[2] name = line_pieces[3] - + # Define and clean listing path: listing_path = '/{}/{}/{}/'.format(storage.id, path, name) listing_path = self.clean_path(listing_path) - + # File or directory? if type == 'directory': if name not in ['.', '..']: @@ -604,12 +604,12 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': name, 'readable': 1, 'writable': 1, - 'path': listing_path + 'path': listing_path } }) else: data.append({ - 'id': listing_path[:-1], # Remove trailing slash + 'id': listing_path[:-1], # Remove trailing slash 'type': 'file', 'attributes':{ 'modified': timestamp, @@ -617,68 +617,68 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'readable': 1, 'writable': 1, "size": size, - 'path': listing_path[:-1] # Remove trailing slash + 'path': listing_path[:-1] # Remove trailing slash } - }) - + }) + else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) - + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + return data def stat(self, path, user, storage): - # Data container + # Data container data = [] if storage.type == 'generic_posix': shell_path = self.sanitize_and_prepare_shell_path(path, user, storage) - + # Prepare command. See the ls function above for some more info command = self.prepare_sh_command('stat --printf=\'%F/%s/%Y/%n\\n\' {}'.format(shell_path), user, storage) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: - + # Did we just get a "cannot stat - No such file or directory error? if 'No such file or directory' in out.stderr: pass else: raise Exception(out.stderr) - - # Log + + # Log #logger.debug('Shell exec output: "{}"'.format(out)) - + out_lines = out.stdout.split('\n') if len(out_lines) > 1: raise Exception('Internal error on stat: more than one ouput line') out_line = out_lines[0] - + # Example output line: directory:My folder:68/1617030350 # In this context, we also might get the following output: # directory/68/1617030350//My folder/ # ..so, use the clean path to remove all extra slashes. # The only uncovered case is to rename the root folder... - + out_line = self.clean_path(out_line) - + # Set names line_pieces = out_line.split('/') type = line_pieces[0] size = line_pieces[1] timestamp = line_pieces[2] name = '/'.join(line_pieces[3:]) - + data = {'type': type, 'name': name, 'size': size, 'timestamp': timestamp} else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) - + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + return data - + def delete(self, path, user, storage): @@ -686,18 +686,18 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): if storage.type == 'generic_posix': shell_path = self.sanitize_and_prepare_shell_path(path, user, storage) - + # Prepare command command = self.prepare_sh_command('rm -rf {}'.format(shell_path), user, storage) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: raise Exception(out.stderr) - + else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) - + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + def mkdir(self, path, user, storage, force=False): @@ -709,28 +709,28 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): command = self.prepare_sh_command('mkdir -p {}'.format(path), user, storage) else: command = self.prepare_sh_command('mkdir {}'.format(path), user, storage) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: raise Exception(out.stderr) - + else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) def cat(self, path, user, storage): - - # Data container + + # Data container data = [] if storage.type == 'generic_posix': - + shell_path = self.sanitize_and_prepare_shell_path(path, user, storage) - + # Prepare command command = self.prepare_sh_command('cat {}'.format(shell_path), user, storage) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: @@ -738,7 +738,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): data = out.stdout else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) return data @@ -746,22 +746,22 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def rename(self, old, new, user, storage): if storage.type == 'generic_posix': - + old = self.sanitize_and_prepare_shell_path(old, user, storage) new = self.sanitize_and_prepare_shell_path(new, user, storage) - + # Prepare command command = self.prepare_sh_command('mv {} {}'.format(old, new), user, storage) - + logger.critical(command) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: raise Exception(out.stderr) - + else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) def copy(self, source, target, user, storage): @@ -770,17 +770,17 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): source = self.sanitize_and_prepare_shell_path(source, user, storage) target = self.sanitize_and_prepare_shell_path(target, user, storage) - + # Prepare command command = self.prepare_sh_command('cp -a {} {}'.format(source, target), user, storage) - + # Execute_command out = os_shell(command, capture=True) if out.exit_code != 0: raise Exception(out.stderr) - + else: - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) def scp_from(self, source, target, user, storage, mode='get'): @@ -815,44 +815,44 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # API GET #============================ def _get(self, request): - + mode = request.GET.get('mode', None) time = request.GET.get('time', None) path = request.GET.get('path', None) _ = request.GET.get('_', None) - + # Clean for some issues that happen sometimes if path: path = self.clean_path(path) - + # Init if mode == 'initiate': data = json.loads('{"data":{"attributes":{"config":{"options":{"culture":"en"},"security":{"allowFolderDownload":true,"extensions":{"ignoreCase":true,"policy":"DISALLOW_LIST","restrictions":[]},"readOnly":false}}},"id":"/","type":"initiate"}}') elif mode == 'readfolder': - + # Base folder (computing resource-level) if path == '/': - - # Data container + + # Data container data = {'data':[]} - + # Get storages storages = list(Storage.objects.filter(group=None,browsable=True)) + list(Storage.objects.filter(group__user=request.user,browsable=True)) - + # Oder storages (re-orderded in the file manager anyway) storages.sort(key=lambda storage: storage.id) - + # Prepare the output for storage in storages: - + # For now, we only support generic posix, SSH-based storages if not storage.type=='generic_posix' and storage.access_mode=='ssh+cli': continue - + if storage.access_through_computing and not storage.computing.manager.is_configured_for(user=request.user): continue - + data['data'].append({ 'id': '/{}/'.format(storage.id), 'type': 'folder', @@ -860,15 +860,15 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': storage.id, 'readable': 1, 'writable': 1, - 'path': '/{}/'.format(storage.id) + 'path': '/{}/'.format(storage.id) } }) - + else: - + storage = self.get_storage_from_path(path, request) - - # Get base directoris and files for this storage: + + # Get base directoris and files for this storage: ls_path = '/'+'/'.join(path.split('/')[2:]) data = {'data': self.ls(ls_path, request.user, storage)} @@ -879,12 +879,12 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Set support vars storage = self.get_storage_from_path(path, request) file_path = '/'+'/'.join(path.split('/')[2:]) - + if path.endswith('/'): return error400('Downloading a folder is not supported') - + if storage.type != 'generic_posix': - raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) + raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type)) # TODO: here we are not handling the ajax request, Maybe they have been deperacted? # The download process consists of 2 requests: @@ -894,23 +894,23 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): if storage.access_mode == 'ssh+cli': target_path = '/tmp/{}'.format(uuid.uuid4()) - + # Get the file - self.scp_from(file_path, target_path, request.user, storage, mode='get') - + self.scp_from(file_path, target_path, request.user, storage, mode='get') + # Detect content type try: content_type = str(magic.from_file(target_path, mime=True)) except: content_type = None - + # Read file data with open(target_path, 'rb') as f: data = f.read() - + # Remove temporary file os.remove(target_path) - + elif storage.access_mode == 'cli': # Define storage internal source path @@ -921,13 +921,13 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): content_type = str(magic.from_file(storage_source_path, mime=True)) except: content_type = None - + # Read file data with open(storage_source_path, 'rb') as f: data = f.read() - + else: - raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode)) + raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode)) # Return file data response = HttpResponse(data, status=status.HTTP_200_OK, content_type=content_type) @@ -936,21 +936,21 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): elif mode == 'readfile': logger.debug('Reading "{}"'.format(path)) - + # Set support vars storage = self.get_storage_from_path(path, request) file_path = '/'+'/'.join(path.split('/')[2:]) # Get file contents data = self.cat(file_path, request.user, storage) - + # Return file contents return HttpResponse(data, status=status.HTTP_200_OK) elif mode == 'delete': logger.debug('Deleting "{}"'.format(path)) - + # Set support vars storage = self.get_storage_from_path(path, request) path = '/'+'/'.join(path.split('/')[2:]) @@ -972,23 +972,23 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': path, 'readable': 1, 'writable': 1, - 'path': '/{}{}'.format(storage.id, path) + 'path': '/{}{}'.format(storage.id, path) } } - } - - + } + + # Return file contents return Response(data, status=status.HTTP_200_OK) - + elif mode == 'addfolder': logger.debug('Deleting "{}"'.format(path)) - + name = request.GET.get('name', None) if not name: raise ValueError('No folder name set') - + # Set support vars storage = self.get_storage_from_path(path, request) path = '/'+'/'.join(path.split('/')[2:]) + name @@ -1005,27 +1005,27 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': name, 'readable': 1, 'writable': 1, - 'path': '/{}{}'.format(storage.id, path) + 'path': '/{}{}'.format(storage.id, path) } } - } - + } + # Return file contents return Response(data, status=status.HTTP_200_OK) elif mode == 'rename': logger.debug('Renaming "{}"'.format(path)) - + # Get old file name with path old_name_with_path = request.GET.get('old', None) if not old_name_with_path: - raise Exception('Missing old name') - + raise Exception('Missing old name') + # Set support vars storage = self.get_storage_from_path(old_name_with_path, request) old_name_with_path = '/'+'/'.join(old_name_with_path.split('/')[2:]) - + # Is it a folder? if old_name_with_path.endswith('/'): is_folder=True @@ -1036,7 +1036,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): new_name = request.GET.get('new', None) if not new_name: raise Exception('Missing new name') - + if is_folder: new_name_with_path = '/'.join(old_name_with_path.split('/')[:-2]) + '/' + new_name old_name_with_path = old_name_with_path[:-1] @@ -1045,11 +1045,11 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Rename self.rename(old_name_with_path, new_name_with_path, request.user, storage) - + # Add trailing slash for listing if is_folder: new_name_with_path = new_name_with_path+'/' - + # Get new info stat = self.stat(new_name_with_path, request.user, storage) @@ -1062,31 +1062,31 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': new_name, 'readable': 1, 'writable': 1, - 'path': '/{}{}'.format(storage.id, new_name_with_path) + 'path': '/{}{}'.format(storage.id, new_name_with_path) } } - } - + } + # Add size if file if not is_folder: data['data']['attributes']['size'] = stat['size'] - + # Return file contents return Response(data, status=status.HTTP_200_OK) elif mode == 'copy': logger.debug('Copying "{}"'.format(path)) - + # Get source for copy source_name_with_path = request.GET.get('source', None) if not source_name_with_path: - raise Exception('Missing source for copy') - + raise Exception('Missing source for copy') + # Get target for copy target_path = request.GET.get('target', None) if not target_path: raise Exception('Missing target for copy') - + if source_name_with_path.endswith('/'): is_folder=True @@ -1096,10 +1096,10 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Set support vars storage = self.get_storage_from_path(source_name_with_path, request) - + if is_folder: source_name_with_path = '/'+'/'.join(source_name_with_path.split('/')[2:])[:-1] - target_name_with_path = '/'+'/'.join(target_path.split('/')[2:]) + source_name_with_path.split('/')[-1] + target_name_with_path = '/'+'/'.join(target_path.split('/')[2:]) + source_name_with_path.split('/')[-1] else: source_name_with_path = '/'+'/'.join(source_name_with_path.split('/')[2:]) target_name_with_path = '/'+'/'.join(target_path.split('/')[2:]) + source_name_with_path.split('/')[-1] @@ -1121,7 +1121,7 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): # Get new info stat = self.stat(target_name_with_path, request.user, storage) - + # Response data data = { 'data': { 'id': '/{}{}'.format(storage.id, target_name_with_path), @@ -1131,25 +1131,25 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'name': target_name_with_path.split('/')[-2] if is_folder else target_name_with_path.split('/')[-1], 'readable': 1, 'writable': 1, - 'path': '/{}{}'.format(storage.id, target_name_with_path) + 'path': '/{}{}'.format(storage.id, target_name_with_path) } } } # Add size if file - if not is_folder: data['data']['attributes']['size'] = stat['size'] - + if not is_folder: data['data']['attributes']['size'] = stat['size'] + # Return file contents return Response(data, status=status.HTTP_200_OK) - + else: return error400('Operation "{}" not supported'.format(mode)) - + return Response(data, status=status.HTTP_200_OK) #============================ - # API POST + # API POST #============================ def _post(self, request): @@ -1160,33 +1160,33 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): if mode == 'savefile': return error400('Operation "{}" not supported'.format(mode)) - + elif mode == 'upload': # Set support vars storage = self.get_storage_from_path(path, request) path = '/'+'/'.join(path.split('/')[2:]) - + # Bug workaround? if not path.endswith('/'): - path += '/' + path += '/' # Get the file upload file_upload = request.FILES['files'] if storage.access_mode == 'ssh+cli': - + # Generate temporary UUID file_uuid = uuid.uuid4() - + with open('/tmp/{}'.format(file_uuid), 'wb') as temp_file: temp_file.write(file_upload.read()) - + logger.debug('Wrote "/tmp/{}" for "{}"'.format(file_uuid, file_upload.name)) - + # Now copy with scp self.scp_to('/tmp/{}'.format(file_uuid), path + file_upload.name , request.user, storage, mode='put') - + # Response data data = { 'data': [{ 'id': '/{}{}{}'.format(storage.id, path, file_upload.name), @@ -1197,19 +1197,19 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'readable': 1, 'size': os.path.getsize('/tmp/{}'.format(file_uuid)), # This is kind of an approximation! 'writable': 1, - 'path': '/{}{}{}'.format(storage.id, path, file_upload.name) + 'path': '/{}{}{}'.format(storage.id, path, file_upload.name) } }] } - + # Remove file - os.remove('/tmp/{}'.format(file_uuid)) - + os.remove('/tmp/{}'.format(file_uuid)) + if storage.access_mode == 'cli': - + try: as_user = storage.conf['as_user'] - + # Is "as_user" a UID? try: uid = int(as_user) @@ -1231,12 +1231,12 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): raise Exception(out.sterr) as_user = 'user_' + str(uid) else: - as_user = out.stdout.strip() - + as_user = out.stdout.strip() + except (KeyError, TypeError): as_user = None - - + + # Define storage internal dest path storage_dest_path = self.sanitize_and_prepare_shell_path(path + file_upload.name, request.user, storage, escapes=False) storage_dest_path_escaped = self.sanitize_and_prepare_shell_path(path + file_upload.name, request.user, storage, escapes=True) @@ -1246,26 +1246,26 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): if as_user: # Generate temporary UUID file_uuid = uuid.uuid4() - + with open('/tmp/{}'.format(file_uuid), 'wb') as temp_file: temp_file.write(file_upload.read()) file_size = os.path.getsize('/tmp/{}'.format(file_uuid)) - + # Change ownership and move os_shell('sudo -i -u {0} chown {0}:{0} /tmp/{1}'.format(as_user, file_uuid), capture=True) if out.exit_code != 0: raise Exception(out.sterr) os_shell('sudo -i -u {0} mv /tmp/{1} {2}'.format(as_user, file_uuid, storage_dest_path_escaped), capture=True) if out.exit_code != 0: - raise Exception(out.sterr) - + raise Exception(out.sterr) + else: with open(storage_dest_path, 'wb') as upload_file: upload_file.write(file_upload.read()) file_size = os.path.getsize(storage_dest_path) - + logger.debug('Wrote "{}" for "{}"'.format(storage_dest_path, file_upload.name)) - + # Response data data = { 'data': [{ 'id': '/{}{}{}'.format(storage.id, path, file_upload.name), @@ -1276,17 +1276,17 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): 'readable': 1, 'size': file_size, 'writable': 1, - 'path': '/{}{}{}'.format(storage.id, path, file_upload.name) + 'path': '/{}{}{}'.format(storage.id, path, file_upload.name) } }] } else: - raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode)) - + raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode)) + # Return return Response(data, status=status.HTTP_200_OK) - + else: return error400('Operation "{}" not supported'.format(mode)) @@ -1303,7 +1303,7 @@ class ImportRepositoryAPI(PrivateGETAPI): """ def _get(self, request): - + repository_url = request.GET.get('repository_url', None) repository_tag = request.GET.get('repository_tag', None) container_name = request.GET.get('container_name', None) @@ -1314,11 +1314,11 @@ class ImportRepositoryAPI(PrivateGETAPI): if not repository_tag: return error400('Missing "repository_tag"') - + logger.debug('Importing repository "%s" with tag "%s"', repository_url, repository_tag) results = {} - try: + try: container = get_or_create_container_from_repository(request.user, repository_url, repository_tag, container_name, container_description) except Exception as e: results['import_succeded'] = False @@ -1326,8 +1326,9 @@ class ImportRepositoryAPI(PrivateGETAPI): else: results['import_succeded'] = True results['container_uuid'] = str(container.uuid) - - return ok200(results) + + return ok200(results) + diff --git a/services/webapp/code/rosetta/core_app/computing_managers.py b/services/webapp/code/rosetta/core_app/computing_managers.py index 8e676a4..af507e5 100644 --- a/services/webapp/code/rosetta/core_app/computing_managers.py +++ b/services/webapp/code/rosetta/core_app/computing_managers.py @@ -12,37 +12,37 @@ ROSETTA_AGENT_CHECK_SSL = booleanize(os.environ.get('ROSETTA_AGENT_CHECK_SSL', T CHECK_CURL_CERT_STR = '--insecure' if not ROSETTA_AGENT_CHECK_SSL else '' class ComputingManager(object): - + def __init__(self, computing): self.computing = computing - + def start_task(self, task, **kwargs): - + # Check for run task logic implementation try: self._start_task except AttributeError: raise NotImplementedError('Not implemented') - + # Call actual run task logic self._start_task(task, **kwargs) def stop_task(self, task, **kwargs): - + # Check for stop task logic implementation try: self._stop_task except AttributeError: raise NotImplementedError('Not implemented') - + # Call actual stop task logic self._stop_task(task, **kwargs) - + # Ok, save status as deleted task.status = 'stopped' task.save() - + # Check if the tunnel is active and if so kill it logger.debug('Checking if task "{}" has a running tunnel'.format(task.uuid)) check_command = 'ps -ef | grep ":'+str(task.tcp_tunnel_port)+':'+str(task.interface_ip)+':'+str(task.interface_port)+'" | grep -v grep | awk \'{print $2}\'' @@ -54,10 +54,10 @@ class ComputingManager(object): tunnel_pid = out.stdout # Kill Tunnel command kill_tunnel_command= 'kill -9 {}'.format(tunnel_pid) - + # Log logger.debug('Killing tunnel with command: {}'.format(kill_tunnel_command)) - + # Execute os_shell(kill_tunnel_command, capture=True) if out.exit_code != 0: @@ -65,16 +65,16 @@ class ComputingManager(object): def get_task_log(self, task, **kwargs): - + # Check for get task log logic implementation try: self._get_task_log except AttributeError: raise NotImplementedError('Not implemented') - + # Call actual get task log logic return self._get_task_log(task, **kwargs) - + def is_configured_for(self, user): return True @@ -88,7 +88,7 @@ class ClusterComputingManager(ComputingManager): class SSHComputingManager(ComputingManager): - + def is_configured_for(self, user): try: get_ssh_access_mode_credentials(self.computing, user) @@ -100,7 +100,7 @@ class SSHComputingManager(ComputingManager): class InternalStandaloneComputingManager(StandaloneComputingManager): - + def _start_task(self, task): # Init run command #--cap-add=NET_ADMIN --cap-add=NET_RAW @@ -121,10 +121,10 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): # Env vars if any if task.container.env_vars: - + # Sanitize again just in case the DB got somehow compromised: env_vars = sanitize_container_env_vars(task.container.env_vars) - + for env_var in env_vars: run_command += ' -e{}={} '.format(env_var, env_vars[env_var]) @@ -136,17 +136,17 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): storages = Storage.objects.filter(computing=self.computing) for storage in storages: if storage.type == 'generic_posix' and storage.bind_path: - + # Expand the base path - expanded_base_path = storage.base_path + expanded_base_path = storage.base_path if '$USER' in expanded_base_path: expanded_base_path = expanded_base_path.replace('$USER', task.user.username) - + # Expand the bind_path - expanded_bind_path = storage.bind_path + expanded_bind_path = storage.bind_path if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) - + # Add the bind if not binds: binds = '-v{}:{}'.format(expanded_base_path, expanded_bind_path) @@ -158,14 +158,14 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): # Debug logger.debug('Running new task with command="{}"'.format(run_command)) - - # Run the task + + # Run the task out = os_shell(run_command, capture=True) if out.exit_code != 0: logger.error('Got error in starting task: {}'.format(out)) raise Exception(out.stderr) else: - + # Get task IP address out = os_shell('export CONTAINER_ID=$(sudo docker ps -a --filter name=task-'+task.short_uuid+' --format {{.ID}}) && sudo docker inspect --format \'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\' $CONTAINER_ID | tail -n1', capture=True) if out.exit_code != 0: @@ -179,7 +179,7 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): # Save task.save() - + # Setup the tunnel if using a custom protocol (otherwise it will get set up via the "connect" button) if task.container.interface_protocol not in ['http', 'https']: setup_tunnel_and_proxy(task) @@ -188,7 +188,7 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): # Delete the Docker container stop_command = 'export CONTAINER_ID=$(sudo docker ps -a --filter name=task-'+task.short_uuid+' --format {{.ID}}) && sudo docker stop $CONTAINER_ID && sudo docker rm $CONTAINER_ID' - + out = os_shell(stop_command, capture=True) if out.exit_code != 0: if 'No such container' in out.stderr: @@ -199,12 +199,12 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): pass else: raise Exception(out.stderr) - + # Set task as stopped task.status = TaskStatuses.stopped task.save() - + def _get_task_log(self, task, **kwargs): # View the Docker container log (attach) @@ -219,7 +219,7 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingManager): - + def _start_task(self, task, **kwargs): logger.debug('Starting a remote task "{}"'.format(self.computing)) @@ -229,7 +229,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana # Get webapp conn string from.utils import get_webapp_conn_string webapp_conn_string = get_webapp_conn_string() - + # Handle container engine container_engine = None if task.computing_options: @@ -237,7 +237,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana if not container_engine: container_engine = task.computing.default_container_engine - # engine-specific part + # engine-specific part if container_engine == 'singularity': #if not task.container.supports_custom_interface_port: @@ -253,20 +253,20 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana varsstring = '' # Sanitize again just in case the DB got somehow compromised: env_vars = sanitize_container_env_vars(task.container.env_vars) - + for env_var in env_vars: varsstring += ' && export SINGULARITYENV_{}={} '.format(env_var, env_vars[env_var]) else: varsstring = '' - + # Handle storages (binds) binds = '' storages = Storage.objects.filter(computing=self.computing) for storage in storages: if storage.type == 'generic_posix' and storage.bind_path: - + # Expand the base path - expanded_base_path = storage.base_path + expanded_base_path = storage.base_path if '$SSH_USER' in expanded_base_path: if storage.access_through_computing: expanded_base_path = expanded_base_path.replace('$SSH_USER', computing_user) @@ -274,9 +274,9 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_base_path: expanded_base_path = expanded_base_path.replace('$USER', task.user.username) - + # Expand the bind_path - expanded_bind_path = storage.bind_path + expanded_bind_path = storage.bind_path if '$SSH_USER' in expanded_bind_path: if storage.access_through_computing: expanded_bind_path = expanded_bind_path.replace('$SSH_USER', computing_user) @@ -284,19 +284,19 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) - + # Add the bind if not binds: binds = '-B {}:{}'.format(expanded_base_path, expanded_bind_path) else: binds += ',{}:{}'.format(expanded_base_path, expanded_bind_path) - + run_command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} '.format(computing_port, computing_keys.private_key_file, computing_user, computing_host) - run_command += '/bin/bash -c \'"rm -rf /tmp/{}_data && mkdir -p /tmp/{}_data/tmp && mkdir -p /tmp/{}_data/home && chmod 700 /tmp/{}_data && '.format(task.uuid, task.uuid, task.uuid, task.uuid) + run_command += '/bin/bash -c \'"rm -rf /tmp/{}_data && mkdir -p /tmp/{}_data/tmp && mkdir -p /tmp/{}_data/home && chmod 700 /tmp/{}_data && '.format(task.uuid, task.uuid, task.uuid, task.uuid) run_command += 'curl {} {}/api/v1/base/agent/?task_uuid={} -o /tmp/{}_data/agent.py &> /dev/null && export BASE_PORT=\$(python3 /tmp/{}_data/agent.py 2> /tmp/{}_data/task.log) && '.format(CHECK_CURL_CERT_STR, webapp_conn_string, task.uuid, task.uuid, task.uuid, task.uuid) run_command += 'export SINGULARITY_NOHTTPS=true && export SINGULARITYENV_BASE_PORT=\$BASE_PORT {} {} &&'.format(authstring, varsstring) 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_name, task.container.image_tag, task.uuid) @@ -313,20 +313,20 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana varsstring = '' # Sanitize again just in case the DB got somehow compromised: env_vars = sanitize_container_env_vars(task.container.env_vars) - + for env_var in env_vars: varsstring += ' -e {}={} '.format(env_var, env_vars[env_var]) else: varsstring = '' - + # Handle storages (binds) binds = '' storages = Storage.objects.filter(computing=self.computing) for storage in storages: if storage.type == 'generic_posix' and storage.bind_path: - + # Expand the base path - expanded_base_path = storage.base_path + expanded_base_path = storage.base_path if '$SSH_USER' in expanded_base_path: if storage.access_through_computing: expanded_base_path = expanded_base_path.replace('$SSH_USER', computing_user) @@ -334,9 +334,9 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_base_path: expanded_base_path = expanded_base_path.replace('$USER', task.user.username) - + # Expand the bind_path - expanded_bind_path = storage.bind_path + expanded_bind_path = storage.bind_path if '$SSH_USER' in expanded_bind_path: if storage.access_through_computing: expanded_bind_path = expanded_bind_path.replace('$SSH_USER', computing_user) @@ -344,26 +344,26 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) - + # Add the bind if not binds: binds = '-v{}:{}'.format(expanded_base_path, expanded_bind_path) else: binds += ' -v{}:{}'.format(expanded_base_path, expanded_bind_path) - + # TODO: remove this hardcoding prefix = 'sudo' if (computing_host == 'slurmclusterworker' and container_engine=='docker') else '' - + run_command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} '.format(computing_port, computing_keys.private_key_file, computing_user, computing_host) - run_command += '/bin/bash -c \'"rm -rf /tmp/{}_data && mkdir /tmp/{}_data && chmod 700 /tmp/{}_data && '.format(task.uuid, task.uuid, task.uuid) + run_command += '/bin/bash -c \'"rm -rf /tmp/{}_data && mkdir /tmp/{}_data && chmod 700 /tmp/{}_data && '.format(task.uuid, task.uuid, task.uuid) run_command += 'curl {} {}/api/v1/base/agent/?task_uuid={} -o /tmp/{}_data/agent.py &> /dev/null && export TASK_PORT=\$(python3 /tmp/{}_data/agent.py 2> /tmp/{}_data/task.log) && '.format(CHECK_CURL_CERT_STR, webapp_conn_string, task.uuid, task.uuid, task.uuid, task.uuid) - run_command += 'exec nohup {} {} run -p \$TASK_PORT:{} {} {} {} '.format(prefix, container_engine, task.container.interface_port, authstring, varsstring, binds) + run_command += 'exec nohup {} {} run -p \$TASK_PORT:{} {} {} {} '.format(prefix, container_engine, task.container.interface_port, authstring, varsstring, binds) if container_engine == 'podman': run_command += '--network=private --uts=private --userns=keep-id ' #run_command += '-d -t {}/{}:{}'.format(task.container.registry, task.container.image_name, task.container.image_tag) run_command += '-h task-{} --name task-{} -t {}/{}:{}'.format(task.short_uuid, task.short_uuid, task.container.registry, task.container.image_name, task.container.image_tag) run_command += '&>> /tmp/{}_data/task.log &"\''.format(task.uuid) - + else: raise NotImplementedError('Container engine {} not supported'.format(container_engine)) @@ -371,8 +371,8 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana if out.exit_code != 0: logger.error('Got error in starting task: {}'.format(out)) raise Exception(out.stderr) - - # Log + + # Log logger.debug('Shell exec output: "{}"'.format(out)) # Load back the task to avoid concurrency problems in the agent call @@ -392,7 +392,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana def _stop_task(self, task, **kwargs): - + # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(self.computing, task.user) @@ -404,7 +404,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana container_engine = task.computing.default_container_engine if container_engine=='singularity': - internal_stop_command = 'kill -9 {}'.format(task.process_id) + internal_stop_command = 'kill -9 {}'.format(task.process_id) elif container_engine in ['docker', 'podman']: # TODO: remove this hardcoding prefix = 'sudo' if (computing_host == 'slurmclusterworker' and container_engine=='docker') else '' @@ -415,7 +415,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana stop_command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} \'/bin/bash -c "{}"\''.format(computing_port, computing_keys.private_key_file, computing_user, computing_host, internal_stop_command) out = os_shell(stop_command, capture=True) - + if out.exit_code != 0: if ('No such process' in out.stderr) or ('No such container' in out.stderr) or ('no container' in out.stderr) or ('missing' in out.stderr): pass @@ -432,7 +432,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(self.computing, task.user) - + # Handle container engine container_engine = None if task.computing_options: @@ -441,13 +441,13 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana container_engine = task.computing.default_container_engine if container_engine=='singularity': - internal_view_log_command = 'cat /tmp/{}_data/task.log'.format(task.uuid) + internal_view_log_command = 'cat /tmp/{}_data/task.log'.format(task.uuid) elif container_engine in ['docker','podman']: # TODO: consider podman/docker logs? internal_view_log_command = 'cat /tmp/{}_data/task.log'.format(task.uuid) else: raise NotImplementedError('Container engine {} not supported'.format(container_engine)) - + # Prepare full comand view_log_command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} \'/bin/bash -c "{}"\''.format(computing_port, computing_keys.private_key_file, computing_user, computing_host, internal_view_log_command) @@ -460,7 +460,7 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManager): - + def _start_task(self, task, **kwargs): logger.debug('Starting a remote task "{}"'.format(self.computing)) @@ -473,7 +473,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag # Initialize sbatch args (force 1 task for now) sbatch_args = '-N1 ' - + # Get task computing parameters and set sbatch args if task.computing_options: task_partition = task.computing_options.get('partition', None) @@ -498,7 +498,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag if not container_engine: container_engine = task.computing.default_container_engine - # engine-specific part + # engine-specific part if container_engine == 'singularity': #if not task.container.supports_custom_interface_port: @@ -514,20 +514,20 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag varsstring = '' # Sanitize again just in case the DB got somehow compromised: env_vars = sanitize_container_env_vars(task.container.env_vars) - + for env_var in env_vars: varsstring += ' && export SINGULARITYENV_{}={} '.format(env_var, env_vars[env_var]) else: varsstring = '' - + # Handle storages (binds) binds = '' storages = Storage.objects.filter(computing=self.computing) for storage in storages: if storage.type == 'generic_posix' and storage.bind_path: - + # Expand the base path - expanded_base_path = storage.base_path + expanded_base_path = storage.base_path if '$SSH_USER' in expanded_base_path: if storage.access_through_computing: expanded_base_path = expanded_base_path.replace('$SSH_USER', computing_user) @@ -535,9 +535,9 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_base_path: expanded_base_path = expanded_base_path.replace('$USER', task.user.username) - + # Expand the bind_path - expanded_bind_path = storage.bind_path + expanded_bind_path = storage.bind_path if '$SSH_USER' in expanded_bind_path: if storage.access_through_computing: expanded_bind_path = expanded_bind_path.replace('$SSH_USER', computing_user) @@ -545,7 +545,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag raise NotImplementedError('Accessing a storage with ssh+cli without going through its computing resource is not implemented') if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) - + # Add the bind if not binds: binds = '-B {}:{}'.format(expanded_base_path, expanded_bind_path) @@ -557,7 +557,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag run_command += 'export SINGULARITY_NOHTTPS=true && export SINGULARITYENV_BASE_PORT=\\\\\\$BASE_PORT {} {} && '.format(authstring, varsstring) run_command += 'rm -rf /tmp/{}_data && mkdir -p /tmp/{}_data/tmp &>> \$HOME/{}.log && mkdir -p /tmp/{}_data/home &>> \$HOME/{}.log && chmod 700 /tmp/{}_data && '.format(task.uuid, task.uuid, task.uuid, task.uuid, task.uuid, task.uuid) 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_name, task.container.image_tag, task.uuid, task.uuid, sbatch_args, task.uuid) @@ -569,7 +569,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag logger.error('Got error in starting task: {}'.format(out)) raise Exception(out.stderr) - # Log + # Log logger.debug('Shell exec output: "{}"'.format(out)) # Parse sbatch output. Example: Output(stdout='Submitted batch job 3', stderr='', exit_code=0) @@ -578,18 +578,18 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag int(job_id) except: raise Exception('Cannot find int job id from output string "{}"'.format(out.stdout)) - + # Load back the task to avoid concurrency problems in the agent call task_uuid = task.uuid task = Task.objects.get(uuid=task_uuid) # Save job id task.job_id = job_id - + # Set status (only fi we get here before the agent which sets the status as running via the API) if task.status != TaskStatuses.running: task.status = TaskStatuses.sumbitted - + # Save task.save() @@ -598,7 +598,7 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag setup_tunnel_and_proxy(task) def _stop_task(self, task, **kwargs): - + # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(self.computing, task.user) @@ -607,14 +607,14 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag out = os_shell(stop_command, capture=True) if out.exit_code != 0: raise Exception(out.stderr) - + # Set task as topped task.status = TaskStatuses.stopped task.save() def _get_task_log(self, task, **kwargs): - + # Get credentials computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(self.computing, task.user) @@ -626,3 +626,4 @@ class SlurmSSHClusterComputingManager(ClusterComputingManager, SSHComputingManag raise Exception(out.stderr) else: return out.stdout + diff --git a/services/webapp/code/rosetta/core_app/decorators.py b/services/webapp/code/rosetta/core_app/decorators.py index d6e13e0..d38473e 100644 --- a/services/webapp/code/rosetta/core_app/decorators.py +++ b/services/webapp/code/rosetta/core_app/decorators.py @@ -51,7 +51,7 @@ def public_view(wrapped_view): error_text = str(e) else: - # Log the exception + # Log the exception logger.error(format_exception(e)) # Raise the exception if we are in debug mode @@ -109,13 +109,13 @@ def private_view(wrapped_view): error_text = str(e) else: - # Log the exception + # Log the exception logger.error(format_exception(e)) - + # Raise the exception if we are in debug mode if settings.DEBUG: raise - + # Otherwise, mask it else: error_text = 'something went wrong ({})'.format(e) @@ -137,3 +137,4 @@ def private_view(wrapped_view): response.set_cookie('post_login_redirect', request.build_absolute_uri()) return response return private_view_wrapper + diff --git a/services/webapp/code/rosetta/core_app/exceptions.py b/services/webapp/code/rosetta/core_app/exceptions.py index 44676fc..12a3bd8 100644 --- a/services/webapp/code/rosetta/core_app/exceptions.py +++ b/services/webapp/code/rosetta/core_app/exceptions.py @@ -3,4 +3,4 @@ class ErrorMessage(Exception): pass class ConsistencyException(Exception): - pass \ No newline at end of file + pass diff --git a/services/webapp/code/rosetta/core_app/fields.py b/services/webapp/code/rosetta/core_app/fields.py index bc59f97..05a90bd 100644 --- a/services/webapp/code/rosetta/core_app/fields.py +++ b/services/webapp/code/rosetta/core_app/fields.py @@ -26,4 +26,4 @@ class JSONField(Field): def value_to_string(self, obj): return self.value_from_object(obj) -# Credits: https://medium.com/@philamersune/using-postgresql-jsonfield-in-sqlite-95ad4ad2e5f1 \ No newline at end of file +# Credits: https://medium.com/@philamersune/using-postgresql-jsonfield-in-sqlite-95ad4ad2e5f1 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 32ff1c1..5dba794 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 @@ -8,13 +8,13 @@ class Command(BaseCommand): def handle(self, *args, **options): - #===================== + #===================== # Testuser #===================== try: testuser = User.objects.get(username='testuser') print('Not creating test user as it already exists') - + except User.DoesNotExist: print('Creating test user with default password') testuser = User.objects.create_user('testuser', 'testuser@rosetta.platform', 'testpass') @@ -22,7 +22,7 @@ class Command(BaseCommand): testuser.is_staff = True testuser.is_admin=True testuser.is_superuser=True - testuser.save() + testuser.save() print('Creating testuser profile') Profile.objects.create(user=testuser, auth='local', authtoken='129aac94-284a-4476-953c-ffa4349b4a50') @@ -32,23 +32,23 @@ class Command(BaseCommand): default = True, private_key_file = '/rosetta/.ssh/id_rsa', public_key_file = '/rosetta/.ssh/id_rsa.pub') - - #===================== + + #===================== # Platform keys #===================== # TODO: create a different pair try: KeyPair.objects.get(user=None, default=True) print('Not creating default platform keys as they already exist') - + except KeyPair.DoesNotExist: print('Creating platform default keys') KeyPair.objects.create(user = None, default = True, private_key_file = '/rosetta/.ssh/id_rsa', public_key_file = '/rosetta/.ssh/id_rsa.pub') - + #===================== # Default home page @@ -60,7 +60,7 @@ class Command(BaseCommand): <h1> Rosetta <img src="/static/img/emoji_u1f6f0.png" style="height:84px; width:64px; padding-bottom:20px"></h1> <h2 style="margin-top:10px; margin-left:25px; margin-right:25px; font-weight:100; line-height: 30px;"><i>A container-centric Science Platform<br></i></h2> </div> - </div> + </div> <div class="container"> <div class="dashboard"> <div class="span8 offset2" style="margin: 30px auto; max-width:800px"> @@ -69,7 +69,7 @@ class Command(BaseCommand): This is the default main page content loaded after populating the platform with the default/demo data. To change it, head to the <a href="/admin">admin</a> section and edit the <code>Page</code> model with id "main". <br/><br/> - A test user with admin rights registered with email <code>testuser@rosetta.platform</code> and password + A test user with admin rights registered with email <code>testuser@rosetta.platform</code> and password <code>testpass</code> has been created as well, which you can use to login on the menu on the right and give Rosetta immediately a try. If you are using the default docker-compose file (i.e. you just ran <code>rosetta/setup</code>), then you will also have a few demo computing and storage resources (beside the internal one) already available @@ -79,10 +79,10 @@ class Command(BaseCommand): <br /> You can also create custom pages and access them under <code>/pages/page_id</code> should you need to provide your users informations about the platform and its storage and computing resources. For example, see this - demo extra <a href="/pages/help">help page</a>. + demo extra <a href="/pages/help">help page</a>. </div> </div> - </div> + </div> </header> ''' home_page = Page.objects.filter(id='main') @@ -96,15 +96,15 @@ class Command(BaseCommand): <h1>Help!</h1> <hr> <p> -This is a demo extra page (a help page, in this case). Here you could for example provide the instructions on how to set up SSH-based +This is a demo extra page (a help page, in this case). Here you could for example provide the instructions on how to set up SSH-based computing resources using user keys, or who to contact to join a specific group to access its software and computing resources. </p> <p> -In general, the part of the URL following the <code>/pages/</code> path is parsed as the page id, +In general, the part of the URL following the <code>/pages/</code> path is parsed as the page id, so that if a page with that id exists in the database, its content will show up here. -You can use this system for creating a mini-website inside the platform -to provide help, news and informations on your deployment. Or you can just ignore the whole thing and leave a plain logo in the main page. +You can use this system for creating a mini-website inside the platform +to provide help, news and informations on your deployment. Or you can just ignore the whole thing and leave a plain logo in the main page. </p> ''' @@ -117,16 +117,16 @@ to provide help, news and informations on your deployment. Or you can just ignor - #===================== + #===================== # Platform containers - #===================== - + #===================== + platform_containers = Container.objects.filter(user=None) if platform_containers: print('Not creating public containers as they already exist') else: print('Creating platform containers...') - + # Minimal Desktop Container.objects.create(user = None, name = 'Minimal Desktop', @@ -157,8 +157,8 @@ to provide help, news and informations on your deployment. Or you can just ignor supports_custom_interface_port = True, supports_interface_auth = True, interface_auth_user = None) - - + + # Jupyter Notebook Container.objects.create(user = None, name = 'Jupyter Notebook', @@ -178,7 +178,7 @@ to provide help, news and informations on your deployment. Or you can just ignor # 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.', @@ -192,7 +192,7 @@ to provide help, news and informations on your deployment. Or you can just ignor 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', @@ -225,9 +225,9 @@ to provide help, news and informations on your deployment. Or you can just ignor supports_interface_auth = True, interface_auth_user = 'metauser') - #===================== + #===================== # Testuser containers - #===================== + #===================== #testuser_containers = Container.objects.filter(user=testuser) #if testuser_containers: # print('Not creating testuser private containers as they already exist') @@ -250,9 +250,9 @@ to provide help, news and informations on your deployment. Or you can just ignor # supports_interface_auth = False) - #===================== + #===================== # Computing resources - #===================== + #===================== computing_resources = Computing.objects.all() if computing_resources: print('Not creating demo computing resources as they already exist') @@ -270,7 +270,7 @@ to provide help, news and informations on your deployment. Or you can just ignor wms = None, container_engines = ['docker']) - + # Demo standalone computing plus conf demo_standalone_computing = Computing.objects.create(name = 'Demo Standalone', description = 'A demo standalone computing resource.', @@ -311,22 +311,22 @@ to provide help, news and informations on your deployment. Or you can just ignor wms = 'slurm', conf = {'host': 'slurmclustermaster', 'default_partition': 'partition1'}, container_engines = ['singularity']) - + # Add testuser extra conf for this computing resource testuser.profile.add_extra_conf(conf_type = 'computing_user', object=demo_slurm_computing, value= 'testuser') - #===================== + #===================== # Storages - #===================== + #===================== storages = Storage.objects.all() if storages: print('Not creating demo storage resources as they already exist') else: print('Creating demo storage resources...') - + # Get demo computing resources demo_computing_resources = [] - try: + try: demo_slurm_computing = Computing.objects.get(name='Demo Cluster') demo_computing_resources.append(demo_slurm_computing) except: @@ -336,8 +336,8 @@ to provide help, news and informations on your deployment. Or you can just ignor demo_computing_resources.append(demo_standalone_computing) except: pass - - + + for computing in demo_computing_resources: # Demo shared storage Storage.objects.create(computing = computing, @@ -348,7 +348,7 @@ to provide help, news and informations on your deployment. Or you can just ignor auth_mode = 'user_keys', base_path = '/shared/data/shared', bind_path = '/storages/shared') - + # Demo personal storage Storage.objects.create(computing = computing, access_through_computing = True, @@ -359,3 +359,4 @@ to provide help, news and informations on your deployment. Or you can just ignor base_path = '/shared/data/users/$SSH_USER', bind_path = '/storages/personal') + diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 76061c1..9d6cb5f 100644 --- a/services/webapp/code/rosetta/core_app/models.py +++ b/services/webapp/code/rosetta/core_app/models.py @@ -37,7 +37,7 @@ class TaskStatuses(object): # using an UUID pointing to some other model instead of the value in future, should this be necessary. #========================= -# Profile +# Profile #========================= class Profile(models.Model): @@ -68,25 +68,25 @@ class Profile(models.Model): self.extra_confs = {} self.extra_confs[str(uuid.uuid4())] = {'type': conf_type, 'object_uuid': str(object.uuid), 'value': value} self.save() - + def get_extra_conf(self, conf_type, object=None): - + if self.extra_confs: for extra_conf in self.extra_confs: if conf_type == self.extra_confs[extra_conf]['type']: if object: #logger.debug("{} vs {}".format(self.extra_confs[extra_conf]['object_uuid'], str(object.uuid))) if self.extra_confs[extra_conf]['object_uuid'] == str(object.uuid): - return self.extra_confs[extra_conf]['value'] + return self.extra_confs[extra_conf]['value'] else: return self.extra_confs[extra_conf]['value'] return None - + #========================= -# Login Token +# Login Token #========================= class LoginToken(models.Model): @@ -106,7 +106,7 @@ class LoginToken(models.Model): class Container(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, related_name='containers', on_delete=models.CASCADE, blank=True, null=True) + user = models.ForeignKey(User, related_name='containers', on_delete=models.CASCADE, blank=True, null=True) # If a container has no user, it will be available to anyone. Can be created, edited and deleted only by admins. 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. @@ -114,25 +114,25 @@ class Container(models.Model): # Generic attributes name = models.CharField('Name', max_length=255, blank=False, null=False) description = models.TextField('Description', blank=True, null=True) - + # Registry registry = models.CharField('Registry', max_length=255, blank=False, null=False) # 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? + + # TODO: do we want more control with respect to kernel, CPUs, instruction sets? # requires = i.e. kernel > 3, intel, AVX2 - + # Port, protocol and transport for the container interface - interface_port = models.IntegerField('Interface port', blank=True, null=True) + interface_port = models.IntegerField('Interface port', blank=True, null=True) interface_protocol = models.CharField('Interface protocol', max_length=36, blank=True, null=True) interface_transport = models.CharField('Interface transport', max_length=36, blank=True, null=True) @@ -156,7 +156,7 @@ class Container(models.Model): # 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 @@ -183,7 +183,7 @@ class Computing(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) group = models.ForeignKey(Group, related_name='computings', on_delete=models.CASCADE, blank=True, null=True) # If a compute resource has no group, it will be available to anyone. Can be created, edited and deleted only by admins. - + name = models.CharField('Name', max_length=255, blank=False, null=False) description = models.TextField('Description', blank=True, null=True) @@ -195,16 +195,16 @@ class Computing(models.Model): 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 engines (e.g. ['docker', 'singularity', 'podman']) container_engines = JSONField('Container engines/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) + 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) + # 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) @@ -233,23 +233,23 @@ class Computing(models.Model): @property def default_container_engine(self): return self.container_engines[0] - + #======================= # Computing manager #======================= - + @property def manager(self): from . import computing_managers - + # Hash table mapping managers_mapping = {} managers_mapping['cluster'+'ssh+cli'+'user_keys'+'slurm'] = computing_managers.SlurmSSHClusterComputingManager managers_mapping['standalone'+'ssh+cli'+'user_keys'+'None'] = computing_managers.SSHStandaloneComputingManager - managers_mapping['standalone'+'ssh+cli'+'platform_keys'+'None'] = computing_managers.SSHStandaloneComputingManager + managers_mapping['standalone'+'ssh+cli'+'platform_keys'+'None'] = computing_managers.SSHStandaloneComputingManager managers_mapping['standalone'+'internal'+'internal'+'None'] = computing_managers.InternalStandaloneComputingManager - + # Instantiate the computing manager and return (if not already done) try: return self._manager @@ -262,10 +262,10 @@ class Computing(models.Model): else: return self._manager - + #========================= -# Tasks +# Tasks #========================= class Task(models.Model): @@ -277,16 +277,16 @@ class Task(models.Model): # Task management status = models.CharField('Status', max_length=36, blank=True, null=True) created = models.DateTimeField('Created on', default=timezone.now) - process_id = models.CharField('Process ID', max_length=64, blank=True, null=True) # i.e. Singularity PID - job_id = models.CharField('Job ID', max_length=64, blank=True, null=True) # i.e. Slurm job id + process_id = models.CharField('Process ID', max_length=64, blank=True, null=True) # i.e. Singularity PID + job_id = models.CharField('Job ID', max_length=64, blank=True, null=True) # i.e. Slurm job id # How to reach the task interface. The IP has to be intended either as the container IP if this is directly # reachable (i.e. using a Docker or Kubernetes network) or as the host IP address, depending on the # computing resource and its computing manager/WMS/container runtime. The port is to be intended # as the port where the task interface is exposed on its IP address. interface_ip = models.CharField('Interface IP address', max_length=36, blank=True, null=True) - interface_port = models.IntegerField('Interface port', blank=True, null=True) - + interface_port = models.IntegerField('Interface port', blank=True, null=True) + # Task access requires_tcp_tunnel = models.BooleanField('Requires TCP tunnel') tcp_tunnel_port = models.IntegerField('TCP tunnel port', blank=True, null=True) @@ -301,13 +301,13 @@ class Task(models.Model): # Computing options # TODO: add the option for selecting the runtime as advanced option when creating the task? computing_options = JSONField('Computing options', blank=True, null=True) # i.e. CPUs, RAM, cluster partition etc. TODO: why here? - - + + class Meta: ordering = ['-created'] def save(self, *args, **kwargs): - + try: getattr(TaskStatuses, str(self.status)) except AttributeError: @@ -318,25 +318,25 @@ class Task(models.Model): def update_status(self): if self.computing == 'local': - + check_command = 'sudo docker inspect --format \'{{.State.Status}}\' ' + self.tid # or, .State.Running out = os_shell(check_command, capture=True) logger.debug('Status: "{}"'.format(out.stdout)) - if out.exit_code != 0: + if out.exit_code != 0: if (('No such' in out.stderr) and (self.tid in out.stderr)): logger.debug('Task "{}" is not running in reality'.format(self.tid)) self.status = TaskStatuses.exited else: if out.stdout == 'running': self.status = TaskStatuses.running - + elif out.stdout == 'exited': self.status = TaskStatuses.exited - + else: raise Exception('Unknown task status: "{}"'.format(out.stdout)) - - self.save() + + self.save() def __str__(self): @@ -351,11 +351,11 @@ class Task(models.Model): string_int_hash = hash_string_to_int(self.name) color_map_index = string_int_hash % len(color_map) return color_map[color_map_index] - + @property def sharable_link(self): return 'https://{}/t/{}'.format(settings.ROSETTA_HOST, self.short_uuid) - + @property def tcp_tunnel_host(self): return get_rosetta_tasks_tunnel_host() @@ -369,32 +369,32 @@ class Task(models.Model): #========================= class Storage(models.Model): - + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) group = models.ForeignKey(Group, related_name='storages', on_delete=models.CASCADE, blank=True, null=True) - + name = models.CharField('Name', max_length=255, blank=False, null=False) #description = models.TextField('Description', blank=True, null=True) - + # Storage type type = models.CharField('Type', max_length=255, blank=False, null=False) - - # Access and auth mode + + # Access and auth mode 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) - + # Paths - base_path = models.CharField('Base path', max_length=4096, blank=False, null=False) - bind_path = models.CharField('Bind path', max_length=4096, blank=True, null=True) - + base_path = models.CharField('Base path', max_length=4096, blank=False, null=False) + bind_path = models.CharField('Bind path', max_length=4096, blank=True, null=True) + # Link with a computing resource computing = models.ForeignKey(Computing, related_name='storages', on_delete=models.CASCADE, blank=True, null=True) # Make optional? access_through_computing = models.BooleanField('Access through linked computing resource?', default=False) # If the above is linked, some configuration can be taken from the linked computing resource (i.e. the hostname) - + # Configuration conf = JSONField(blank=True, null=True) - + # Include as browsable in the file manager? browsable = models.BooleanField('Browsable in the file manager?', default=True) @@ -405,29 +405,29 @@ class Storage(models.Model): class Meta: ordering = ['name'] - + def __str__(self): if self.group: return str('Storage "{}" of group "{}"'.format(self.id, self.group)) else: return str('Storage "{}"'.format(self.id)) - + @property def id(self): return (self.name if not self.computing else '{}:{}'.format(self.computing.name,self.name)) - + #========================= -# KeyPair +# KeyPair #========================= class KeyPair(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, related_name='key_pairs', on_delete=models.CASCADE, blank=True, null=True) + user = models.ForeignKey(User, related_name='key_pairs', on_delete=models.CASCADE, blank=True, null=True) private_key_file = models.CharField('Private key file', max_length=4096, blank=False, null=False) public_key_file = models.CharField('Public key file', max_length=4096, blank=False, null=False) @@ -440,12 +440,12 @@ class KeyPair(models.Model): return str('KeyPair of user "{}" (default={})'.format( self.user.email, self.default)) else: return str('KeyPair of user Platform (default={})'.format(self.default)) - + #========================= -# Page +# Page #========================= class Page(models.Model): @@ -461,3 +461,4 @@ class Page(models.Model): + diff --git a/services/webapp/code/rosetta/core_app/templates/account.html b/services/webapp/code/rosetta/core_app/templates/account.html index b8f07fe..051caeb 100644 --- a/services/webapp/code/rosetta/core_app/templates/account.html +++ b/services/webapp/code/rosetta/core_app/templates/account.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -23,24 +23,24 @@ {{data.user.username}} </td> </tr> - + <tr> <td> <b>Email</b> </td> <td> - {% if data.edit == 'email' %} + {% if data.edit == 'email' %} <table class="frametab"><tr><td> <input type='hidden' name='edit' value='email' /> <input type="email" value="{{data.user.email}}" name='value' style='width:95%' required autofocus /> </td><td> <input type="submit" value="Go"> - </td></tr></table> + </td></tr></table> {% else %} {{data.user.email}}{% if data.user.profile.auth == 'local' %} | <a href="/account/?edit=email">Change</a>{% endif %} {% endif %} </td> - </tr> + </tr> {% if data.user.profile.auth == 'local' %} <tr> @@ -48,19 +48,19 @@ <b>Password</b> </td> <td> - {% if data.edit == 'password' %} + {% if data.edit == 'password' %} <table class="frametab"><tr><td> <input type='hidden' name='edit' value='password' /> <input type="password" name='value' style='width:95%' required autofocus /> </td><td> <input type="submit" value="Go"> - </td></tr></table> + </td></tr></table> {% else %} ******* | <a href="/account/?edit=password">Change</a> {% endif %} </td> </tr> - {% endif %} + {% endif %} <tr> @@ -75,24 +75,24 @@ {% else %} {{ data.user.profile.auth }} {% endif %} - </td> + </td> </tr> </table> <br /> - + <h3>Profile</h3> <table class="dashboard"> - + <tr> <td> <b>Time Zone</b> </td> <td> - - {% if data.edit == 'timezone' %} + + {% if data.edit == 'timezone' %} {% include "components/tzselect.html" %} <input type='hidden' name='edit' value='timezone' /> <input type="submit" value="Go"> @@ -101,7 +101,7 @@ {% endif %} </td> </tr> - + <tr> <td valign=top> @@ -117,23 +117,23 @@ {% endif %} {% endfor %} {% endif %} - + : <code>{{conf_data.value}}</code> | <a href='?delete_extra_conf_uuid={{conf_uuid}}'>delete</a> - <br/> + <br/> {% endfor %} - - - + + + <span style="margin:3px"><a href="/add_profile_conf" style="line-height:2em">Add new...</a></span> </td> - </tr> + </tr> </table> <br /> <h3>KeyPair</h3> <table class="dashboard"> - + <tr> <td valign="top"> <b>Default public key</b> @@ -145,7 +145,7 @@ </table> </form> - + <div style="margin-left:10px; margin-top:40px"> {% if data.user.profile.auth == 'oidc' %} <form action="{% url 'oidc_logout' %}" method="post"> @@ -155,18 +155,18 @@ {% else %} <form action="/logout/" method="get"> <input type="submit" value="logout"> - </form> + </form> {% endif %} </div> - + <br/> <br/> <br/> <br/> <br/> <br/> - + </div> </div> </div> @@ -178,3 +178,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/add_profile_conf.html b/services/webapp/code/rosetta/core_app/templates/add_profile_conf.html index 56e275f..1c70116 100644 --- a/services/webapp/code/rosetta/core_app/templates/add_profile_conf.html +++ b/services/webapp/code/rosetta/core_app/templates/add_profile_conf.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,19 +6,19 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - <h1>Add extra profile configuration</h1> + <h1>Add extra profile configuration</h1> <hr> - <h4>Chose the configuration type and add the values</h4> - + <h4>Chose the configuration type and add the values</h4> + <br/> - + <form action="/add_profile_conf/" method="POST"> {% csrf_token %} - + <table class="dashboard" style="max-width:430px"> <tr> - + <td> {%if data.conf_type %} <select name="conf_type"> @@ -29,7 +29,7 @@ {% for conf_type in data.conf_types %} <option value="{{conf_type}}">{{conf_type}}</option> {% endfor %} - </select> + </select> {% endif %} </td> @@ -44,11 +44,11 @@ {% for computing in data.computings %} <option value="{{computing.uuid}}">{{computing.name}}</option> {% endfor %} - </select> + </select> {% endif %} </td> {% endif %} - + {% if data.last_step %} <td colspan=2 align=center style="padding:20px"> @@ -62,11 +62,10 @@ <input type="submit" value="Next"> </td> {% endif %} - + </table> </form> - <br/> <br/> <br/> <br/> @@ -75,7 +74,8 @@ <br/> <br/> <br/> - + <br/> + </div> </div> </div> @@ -87,3 +87,4 @@ + 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 2ef2970..ecef3eb 100644 --- a/services/webapp/code/rosetta/core_app/templates/add_software.html +++ b/services/webapp/code/rosetta/core_app/templates/add_software.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,24 +6,24 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - <h1>Add software container</h1> + <h1>Add software container</h1> <hr> {% if not data.added %} Here you can add a new software container on the platform. You can add containers from image registries as <a href="https://hub.docker.com/">Docker Hub </a>or by importing Git repositories, provided that they - are compatible with <a href="https://mybinder.readthedocs.io/en/latest/introduction.html">Binder</a> specifications. + are compatible with <a href="https://mybinder.readthedocs.io/en/latest/introduction.html">Binder</a> specifications. <br/> <br/> - + {% if data.new_container_from == 'registry' %} <div style="font-size:1.1em; background:whitesmoke; display:inline-block; padding:2px 15px 2px 15px">New container from registry</div> - <div style="font-size:1.1em; background:white; display:inline-block; padding:2px 15px 2px 15px"><a href="?new_container_from=repository">New container from Git repository</a></div> + <div style="font-size:1.1em; background:white; display:inline-block; padding:2px 15px 2px 15px"><a href="?new_container_from=repository">New container from Git repository</a></div> <hr style="margin-top:0;"> - <h4>Basics</h4> - + <h4>Basics</h4> + <form action="#" method="POST"> {% csrf_token %} @@ -43,7 +43,7 @@ ​<textarea name="container_description" rows="3" cols="22"></textarea> </td> </tr> - + <tr> <td><b>Registry</b></td><td> <input type="text" name="container_registry" value="docker.io" size="23" required /> @@ -68,7 +68,7 @@ - <h4>Interface </h4> + <h4>Interface </h4> <table class="dashboard" style="width:400px; margin-bottom:25px"> <tr> @@ -80,7 +80,7 @@ <td><b>Interface protocol</b></td> <td> {% if request.user.profile.is_power_user %} - <input type="text" value="http" name="container_interface_protocol" size="5" /> + <input type="text" value="http" name="container_interface_protocol" size="5" /> {% else %} <select name="container_interface_protocol" > <option value="http" selected>http</option> @@ -91,13 +91,13 @@ </tr> </table> - + <a href="javascript:void(0);" id="show_button" onclick="toggle_visibility('advanced_div')">Advanced...</a> - + <div id="advanced_div" style="display:none; width:400px;"> <h4>Advanced <font size=-1>| <a href="javascript:void(0);" id="hide_button" onclick="toggle_visibility('advanced_div')" style="display:none">hide</a></font></h4> - + <table class="dashboard" style="width:400px; margin-bottom:25px"> <tr> @@ -130,21 +130,21 @@ <tr> <td colspan=2> - <b>Supports custom interface port</b> + <b>Supports custom interface port</b> <input type="checkbox" name="container_supports_custom_interface_port" value="True" /> </td> </tr> <tr> <td colspan=2> - <b>Supports interface auth</b> + <b>Supports interface auth</b> <input type="checkbox" name="container_supports_interface_auth" value="True" /> </td> </tr> <tr> <td colspan=2> - <b>Disable HTTP auth embedding</b> + <b>Disable HTTP auth embedding</b> <input type="checkbox" name="container_disable_http_basicauth_embedding" value="True" /> </td> </tr> @@ -158,7 +158,7 @@ </table> </div> - + <table style="width:400px; border:0; background:#ffffff; margin-top:20px"> <tr><td align="center"> <input type="submit" value="Add"> @@ -166,17 +166,17 @@ </table> <input type="hidden" name="new_container_from" value="registry"> - + </form> {% else %} <div style="font-size:1.1em; background:white; display:inline-block; padding:2px 15px 2px 15px"><a href="?new_container_from=registry">New container from registry</a></div> - <div style="font-size:1.1em; background:whitesmoke; display:inline-block; padding:2px 15px 2px 15px">New container from Git repository</div> + <div style="font-size:1.1em; background:whitesmoke; display:inline-block; padding:2px 15px 2px 15px">New container from Git repository</div> <hr style="margin-top:0;"> - <h4>Basics</h4> - + <h4>Basics</h4> + <form action="#" method="POST"> {% csrf_token %} @@ -196,7 +196,7 @@ ​<textarea name="container_description" rows="3" cols="22"></textarea> </td> </tr> - + <tr> <td><b>Repository URL</b></td><td> <input type="text" name="repository_url" size="23" required /> @@ -214,24 +214,23 @@ </table> - + <table style="width:400px; border:0; background:#ffffff; margin-top:20px"> <tr><td align="center"> <input type="submit" value="Add"> </td></tr> </table> - + <input type="hidden" name="new_container_from" value="repository"> - - </form> - - - + + </form> + + + {% endif %} - <br/> <br/> <br/> <br/> @@ -240,21 +239,22 @@ <br/> <br/> <br/> - - + <br/> + + {% else %} Ok, software container added. Go to <a href="/software">software</a>. - - {% endif %} - + + {% endif %} + <br/> <br/> <br/> <br/> <br/> <br/> - + </div> </div> </div> @@ -270,7 +270,7 @@ function toggle_visibility(id) { x.style.display = "block"; y.style.display = "none" z.style.display= "inline" - + } else { x.style.display = "none"; y.style.display= "inline" @@ -283,3 +283,4 @@ function toggle_visibility(id) { + diff --git a/services/webapp/code/rosetta/core_app/templates/backend.html b/services/webapp/code/rosetta/core_app/templates/backend.html index e679084..757e34e 100644 --- a/services/webapp/code/rosetta/core_app/templates/backend.html +++ b/services/webapp/code/rosetta/core_app/templates/backend.html @@ -13,10 +13,10 @@ <div style="color:green"> {{data.success}} </div> - {% else %} + {% else %} <br/> - {% endif %} - + {% endif %} + {% if request.user.is_authenticated %} Logged in as <b>{{ request.user.username }}</b> | <a href="/logout">Logout</a> {% else %} @@ -41,11 +41,12 @@ <li><a href="/api/v1/base/login/">/api/v1/base/login/</a></li> <li><a href="/api/v1/base/logout/">/api/v1/base/logout/</a></li> </ul> - + </div> - + </body> - + </html> + 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 8e709da..8b3826f 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/computing.html +++ b/services/webapp/code/rosetta/core_app/templates/components/computing.html @@ -1,5 +1,5 @@ - {% if data.computing %} + {% if data.computing %} <div style="float:left; width:500px; margin:10px; margin-bottom:20px"> <table class="dashboard" style="width:100%"> @@ -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> @@ -73,7 +73,7 @@ </table> </div> - + {% else %} @@ -95,10 +95,10 @@ {% if not computing_configured %} <div style="width:298px; float:left; margin:0; margin-bottom:5px; padding:2px; background: gary; color:black; text-align:center; border-bottom: #e0e0e0 solid 1px;"> <i class="fa fa-info-circle"></i> Not configured <!-- style="color:#337ab7" --> - </div> + </div> {% endif %} - + {% if not computing_configured %} <!-- <div style="padding:10px; -webkit-filter: blur(5px); -moz-filter: blur(5px); -o-filter: blur(5px); -ms-filter: blur(5px); filter: blur(5px);"> --> @@ -107,12 +107,12 @@ <div style="padding:10px;"> {% endif %} - {% if computing.description %} + {% if computing.description %} <div class="description-box" title="{{ computing.description }}"> {{ computing.description }} </div> {% else %} - <br/> + <br/> {% endif %} <div class="image-version-box"> <b>Type:</b> {{ computing.type }} <font style="font-size:0.9em">({{ computing.arch }})</font> @@ -133,7 +133,7 @@ <!-- ><b>Access:</b> {{ computing.access_mode }}<br/> -- <!-- <b>Owner:</b> {% if computing.user %}{{ computing.user }}{% else %}Platform{% endif %}<br/> --> <!-- <b>Engines:</b> {{ computing.container_engines }} --> - + <!-- {% if 'docker' in computing.container_engines %}<img src="/static/img/docker-logo.svg" style="height:18px; width:18px; margin-bottom:2px" />{% endif %} {% if 'singularity' in computing.container_engines %}<img src="/static/img/singularity-logo.svg" style="height:18px; width:18px; margin-bottom:2px" />{% endif %}--> {% if container %} @@ -152,15 +152,15 @@ {% endif %} </div> {% endif %} - - </div> + + </div> </div> {% endif %} - - + + {% endif %} - - - \ No newline at end of file + + + 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 00d561d..eec4445 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/container.html +++ b/services/webapp/code/rosetta/core_app/templates/components/container.html @@ -1,6 +1,6 @@ - {% if details %} + {% if details %} <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"> @@ -8,7 +8,7 @@ <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> </tr> @@ -43,7 +43,7 @@ <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> @@ -85,33 +85,33 @@ <tr> <td colspan=2> - <b>Supports custom interface port</b> + <b>Supports custom interface port</b> {% if container.supports_custom_interface_port %} - <input type="checkbox" name="container_supports_user_auth" checked disabled/> + <input type="checkbox" name="container_supports_user_auth" checked disabled/> {% else %} - <input type="checkbox" name="container_supports_user_auth" disabled/> + <input type="checkbox" name="container_supports_user_auth" disabled/> {% endif %} </td> </tr> <tr> <td colspan=2> - <b>Supports interface auth</b> + <b>Supports interface auth</b> {% if container.supports_interface_auth %} - <input type="checkbox" name="supports_interface_auth" checked disabled/> + <input type="checkbox" name="supports_interface_auth" checked disabled/> {% else %} - <input type="checkbox" name="supports_interface_auth" disabled/> + <input type="checkbox" name="supports_interface_auth" disabled/> {% endif %} </td> </tr> <tr> <td colspan=2> - <b>Disable HTTP auth embedding</b> + <b>Disable HTTP auth embedding</b> {% if container.disable_http_basicauth_embedding %} - <input type="checkbox" name="disable_http_basicauth_embedding" checked disabled/> + <input type="checkbox" name="disable_http_basicauth_embedding" checked disabled/> {% else %} - <input type="checkbox" name="disable_http_basicauth_embedding" disabled/> + <input type="checkbox" name="disable_http_basicauth_embedding" disabled/> {% endif %} </td> </tr> @@ -122,7 +122,7 @@ <td><pre>{{container.env_vars}}</pre></td> </tr> {% endif %} - + {% if container.user %} <tr> <td><b>Operations</b></td> @@ -131,26 +131,26 @@ {% endif %} </table> </div> - - + + {% else %} <div style="width:300px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:15px"> - + <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; "> - - {% if container.description %} + + {% if container.description %} <div class="description-box" title="{{ container.description }}"> {{ container.description }} </div> {% else %} - <br/> + <br/> {% endif %} - + <div class="image-version-box"> <!-- <font style="font-family:monospace; font-size:1.2em"></font> --> <b>Image:</b> <code>{{ container.image_name }}</code><br/> @@ -158,22 +158,22 @@ {% if container.image_arch %} <font style="font-size:0.9em">({{ container.image_arch }})</font> {% endif %} - + <!-- <b>Arch:</b> {{ container.image_arch }} --> - </div> - </div> - + + </div> + <div style="margin-bottom:18px; margin-top: 3px; text-align:center"> {% if not disable_play_button %} <a href="/new_task?task_container_uuid={{ container.uuid }}&step=two" class="btn btn-light" style="border: #c0c0c0 1px solid"> <i class="fa fa-play" style="color:green"></i></a> {% endif %} - </div> - - - + </div> + + + </div> - + {% endif %} @@ -181,4 +181,4 @@ - \ No newline at end of file + 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 2dc88c6..e39dcbe 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 @@ -3,33 +3,33 @@ {% for container in container_family.members %} {% include "components/container.html" with container=container details=data.details %} {% endfor %} - - {% else %} + + {% 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> + <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 %} + + {% if container_family.description %} <div class="description-box" title="{{ container_family.description }}"> {{ container_family.description }} </div> {% else %} - <br/> + <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 %} @@ -79,32 +79,32 @@ </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; padding-left:14px"> <i class="fa fa-play" style="color:green"></i> </button> - + {% endif %} </form> - </div> - - - + </div> + + + </div> - + {% endif %} @@ -112,4 +112,4 @@ - \ 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 6c075a3..ffbc84a 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/task.html +++ b/services/webapp/code/rosetta/core_app/templates/components/task.html @@ -1,34 +1,34 @@ - {% if data.task %} + {% if data.task %} <center> <div style="width:350px; display:block; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:20px"> <div style="margin-top:5px; padding:10px; text-align:center; border-bottom: #f8f8f8 solid 1px; "> <b>{{data.task.name}}</b> </div> <hr style="margin:0"> - + {% else %} <div style="width:350px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:20px"> - + <div style="margin-top:5px; padding:10px; text-align:center; border-bottom: #e0e0e0 solid 1px; "> <a href="?uuid={{ task.uuid }}"><b>{{ task.name }}</b></a> </div> {% endif %} - - + + <!-- <div style="background:{{task.color}}; height:24px;"></div> --> <!-- <div style="background:{{ task.container.color }}; height:14px;"></div> --> - + <!-- <div style="margin-bottom:15px; margin-top:0px"> <div style="background:{{task.container.color}}; height:14px; width:149px; float:left"></div> <div style="background:{{task.computing.color}}; height:14px; width:149px; float:left"></div> </div> --> - + <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/> --> @@ -39,68 +39,68 @@ {% 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> - <!-- <a href="/computing/?uuid={{ task.computing.uuid }}" no_style="color:{{task.computing.color}}"><i class="fa fa-external-link" ></i></a><br/> --> - + <!-- <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"> {% if task.status == "running" %} <b>Status:</b> <font color="green">running</font> {% if task.container.interface_protocol == 'http' or task.container.interface_protocol == 'https' %} {% else %} - @ port {{ task.tcp_tunnel_port }} + @ port {{ task.tcp_tunnel_port }} {% endif %} - - + + {% else %} <b>Status:</b> {{ task.status }} {% endif %} </div> </div> - + <div style="margin-bottom:10px; text-align:center; padding:5px"> - + <!-- Stop / Delete / Cancel --> {% if task.status == "stopped" or task.status == "created" %} <a href="?uuid={{task.uuid}}&action=delete&fromlist=True" class="btn btn-action">Delete</a> - {% else %} + {% else %} <a href="?uuid={{task.uuid}}&action=stop&fromlist=True" class="btn btn-action">Stop</a> {% endif %} - + <!-- Connect --> {% if task.interface_port %} {% if task.container.interface_protocol == 'http' or task.container.interface_protocol == 'https' %} {% if task.status == "running" %} <a href="/task_connect/?uuid={{task.uuid}}" class="btn btn-connect">Connect</a> {% else %} - <a href="" class="btn btn-disabled">Connect</a> + <a href="" class="btn btn-disabled">Connect</a> {% endif %} {% endif %} {% endif %} - + <!-- View log --> {% if task.status == "running" %} - <a href="/task_log/?uuid={{task.uuid}}&action=viewlog" class="btn btn-action">Logs</a> + <a href="/task_log/?uuid={{task.uuid}}&action=viewlog" class="btn btn-action">Logs</a> {% else %} - <a href="" class="btn btn-disabled">Logs</a> + <a href="" class="btn btn-disabled">Logs</a> {% endif %} - </div> - - - + </div> + + + </div> - {% if data.task %} + {% if data.task %} <br/> - + <a href="javascript:void(0);" id="show_button" onclick="toggle_visibility('details_div')">Details..</a> - + <div id="details_div" style="display:none; width:400px;"> <b>Details <font size=-1>| <a href="javascript:void(0);" id="hide_button" onclick="toggle_visibility('details_div')" style="display:none">hide</a></font></b> - - + + <div style="display:block"; max-width:450px"> <table class="dashboard" style="margin:10px"> @@ -132,7 +132,7 @@ <tr> <td><b>Status</b></td> <td>{{ task.status }}</td> - </tr> + </tr> <tr> <td><b>Created at</b></td> @@ -148,7 +148,7 @@ <td><b>Interface port</b></td> <td>{{ task.interface_port }}</td> </tr> - + <tr> <td><b>Tunnel port</b></td> <td>{{ task.tcp_tunnel_port }}</td> @@ -160,17 +160,17 @@ <td>{{ task.auth_token }}</td> </tr> {% endif %} - + {% if task.computing_options %} <tr> <td><b>Computing options</b></td> <td>{{ task.computing_options }}</td> </tr> - {% endif %} + {% endif %} <!-- <tr><td style="padding-right:0"><b>Direct link</b> - <td>{% if task.status == "running" %}<a href="{{ task.sharable_link }}">{{ task.sharable_link }}</a>{% else %}N.A. (task not running) {% endif %}</td> + <td>{% if task.status == "running" %}<a href="{{ task.sharable_link }}">{{ task.sharable_link }}</a>{% else %}N.A. (task not running) {% endif %}</td> </tr>--> @@ -178,7 +178,7 @@ </table> </div> - + <!-- <p style="margin-left:10px; font-size:0.9em; color:#484848"> <i class="fa fa-info-circle" style="color:#337ab7"></i> You can share a direct link with other people, but remember that if you have no authentication in place anyone will be able to access. </p> --> @@ -186,5 +186,5 @@ </center> - - {% endif %} \ No newline at end of file + + {% endif %} diff --git a/services/webapp/code/rosetta/core_app/templates/components/tzselect.html b/services/webapp/code/rosetta/core_app/templates/components/tzselect.html index 18b5f10..18ddf0f 100644 --- a/services/webapp/code/rosetta/core_app/templates/components/tzselect.html +++ b/services/webapp/code/rosetta/core_app/templates/components/tzselect.html @@ -1,7 +1,7 @@ <select name="value" > <option disabled selected style='display:none;'>Time Zone...</option> - <optgroup label="US (Common)"> + <optgroup label="US (Common)"> <option value="America/Puerto_Rico">Puerto Rico (Atlantic)</option> <option value="America/New_York">New York (Eastern)</option> <option value="America/Chicago">Chicago (Central)</option> @@ -232,7 +232,7 @@ <option value="Europe/Zaporozhye">Zaporozhye</option> <option value="Europe/Zurich">Zurich</option> </optgroup> - + <optgroup label="Asia"> <option value="Asia/Aden">Aden</option> <option value="Asia/Almaty">Almaty</option> @@ -379,7 +379,7 @@ <option value="Africa/Tunis">Tunis</option> <option value="Africa/Windhoek">Windhoek</option> </optgroup> - + <optgroup label="Australia"> <option value="Australia/ACT">ACT</option> <option value="Australia/Adelaide">Adelaide</option> @@ -419,7 +419,7 @@ <option value="Indian/Mayotte">Mayotte</option> <option value="Indian/Reunion">Reunion</option> </optgroup> - + <optgroup label="Atlantic"> <option value="Atlantic/Azores">Azores</option> <option value="Atlantic/Bermuda">Bermuda</option> @@ -477,7 +477,7 @@ <option value="Pacific/Wallis">Wallis</option> <option value="Pacific/Yap">Yap</option> </optgroup> - + <optgroup label="Antarctica"> <option value="Antarctica/Casey">Casey</option> <option value="Antarctica/Davis">Davis</option> @@ -529,5 +529,5 @@ <option value="UTC+13">UTC+13</option> <option value="UTC+14">UTC+14</option> </optgroup> - -</select> \ No newline at end of file + +</select> diff --git a/services/webapp/code/rosetta/core_app/templates/computing.html b/services/webapp/code/rosetta/core_app/templates/computing.html index 41bec5c..35a9c65 100644 --- a/services/webapp/code/rosetta/core_app/templates/computing.html +++ b/services/webapp/code/rosetta/core_app/templates/computing.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,25 +6,25 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - + {% if data.computing %} - <h1><a href="/computing">Computing resources</a> <span style="font-size:18px"> / {{ data.computing.name }}</span></h1> + <h1><a href="/computing">Computing resources</a> <span style="font-size:18px"> / {{ data.computing.name }}</span></h1> {% else %} <h1>Computing resources</h1> {% endif %} - + <hr/> <div class="row" style="padding:5px"> {% if data.computing %} {% include "components/computing.html" with computing=data.computing details=True %} - {% else %} + {% else %} {% for computing in data.computings %} {% include "components/computing.html" with computing=computing %} {% endfor %} {% endif %} </div> - - <!-- <div class="row" style="padding:10px; padding-left:15px"> + + <!-- <div class="row" style="padding:10px; padding-left:15px"> <a href="/add_container">Add new...</a> </div> --> @@ -45,3 +45,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/error.html b/services/webapp/code/rosetta/core_app/templates/error.html index 2c88790..9bf49bb 100644 --- a/services/webapp/code/rosetta/core_app/templates/error.html +++ b/services/webapp/code/rosetta/core_app/templates/error.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -10,8 +10,8 @@ <hr> <br/> <br/><br/> - <div class='centerbox-error-outer'> - <span class='centerbox-error-inner'> + <div class='centerbox-error-outer'> + <span class='centerbox-error-inner'> {{data.error}} </span> </div> @@ -22,7 +22,7 @@ <br/> <br/> <br/> - + </div> </div> </div> @@ -33,3 +33,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/footer.html b/services/webapp/code/rosetta/core_app/templates/footer.html index 6e4daa4..7c9cbbc 100644 --- a/services/webapp/code/rosetta/core_app/templates/footer.html +++ b/services/webapp/code/rosetta/core_app/templates/footer.html @@ -42,13 +42,14 @@ colour_rgb = hexToRgb(colour) - return "rgba(" + colour_rgb.r + "," + colour_rgb.g + "," + colour_rgb.b + ",0.3)" - + return "rgba(" + colour_rgb.r + "," + colour_rgb.g + "," + colour_rgb.b + ",0.3)" + } </script> - - + + </body> </html> + diff --git a/services/webapp/code/rosetta/core_app/templates/header.html b/services/webapp/code/rosetta/core_app/templates/header.html index 8e26792..7161aab 100644 --- a/services/webapp/code/rosetta/core_app/templates/header.html +++ b/services/webapp/code/rosetta/core_app/templates/header.html @@ -12,7 +12,7 @@ {% if refresh %} <meta http-equiv="refresh" content="{{refresh}}" > {% endif %} - + {% if title %} <title>Rosetta - {{ title }}</title> {% else %} @@ -49,10 +49,10 @@ <!-- jQuery --> <script src="/static/js/jquery-1.11.1.js"></script> - + <!-- Bootstrap Core JavaScript --> <script src="/static/js/bootstrap-3.3.5.min.js"></script> - + <!-- Moment Javascript --> <script src="/static/js/moment-with-locales-2.22.2.js"></script> @@ -62,4 +62,4 @@ </head> <body {{ body_args }}> - \ No newline at end of file + diff --git a/services/webapp/code/rosetta/core_app/templates/import_repository.html b/services/webapp/code/rosetta/core_app/templates/import_repository.html index eb4346d..4e6476b 100644 --- a/services/webapp/code/rosetta/core_app/templates/import_repository.html +++ b/services/webapp/code/rosetta/core_app/templates/import_repository.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,24 +6,24 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - {% if data.mode == 'new_task' %} - <h1>New Task</h1> + {% if data.mode == 'new_task' %} + <h1>New Task</h1> {% elif data.mode == 'new_container' %} - <h1>Add software container</h1> + <h1>Add software container</h1> {% else %} - <h1>Importing repository</h1> + <h1>Importing repository</h1> {% endif %} <hr> <div id="importing"> - Importing repository <code>{{ data.repository_url }}</code>... + Importing repository <code>{{ data.repository_url }}</code>... <br> <br> <br> <br> <center><img src="/static/img/ajax-loader.gif"></center> </div> - + <div id="import_succeded" style="display:none"> Ok, software container added. Go to <a href="/software">software</a>. </div> @@ -32,7 +32,7 @@ <div id="import_failed" style="display:none"> FAILED: <span id="import_failed_text"></span> </div> - + <script type="text/javascript"> var apiUrl = '/api/v1/import_repository/?repository_url={{data.repository_url}}&repository_tag={{data.repository_tag}}&container_name={{data.container_name}}&container_description={{data.container_description}}'; fetch(apiUrl).then(response => { @@ -45,21 +45,21 @@ }).then(data => { // Work with JSON data here console.log(data) - + if (data.results.import_succeded){ - + // Terrbile django templating mixed with javascript - {% if data.mode == 'new_task' %} - - window.location.replace("/new_task/?step=two&task_container_uuid="+data.results.container_uuid) - + {% if data.mode == 'new_task' %} + + window.location.replace("/new_task/?step=two&task_container_uuid="+data.results.container_uuid) + {% else %} - + $("#importing").hide(); - $("#import_succeded").show(); - + $("#import_succeded").show(); + {% endif %} - + } else { throw data.results.error_message } @@ -72,7 +72,7 @@ $("#import_failed_text").html(error); }); </script> - + </div> </div> </div> @@ -89,3 +89,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/login.html b/services/webapp/code/rosetta/core_app/templates/login.html index 40bd704..80e40a7 100644 --- a/services/webapp/code/rosetta/core_app/templates/login.html +++ b/services/webapp/code/rosetta/core_app/templates/login.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html"%} {% include "logo.html" %} @@ -17,44 +17,44 @@ <input type="password" class="form-control" placeholder="Password" name='password'> <br /> <input type='submit' style="width:110px" class="btn btn-lg btn-success btn-block" value='Log in' /> - + <div style="margin-top:25px; margin-bottom:15px"> {% if OPENID_ENABLED %}<font color="#a9a9a9"> — OR —</font>{% endif %} </div> - + {% if OPENID_ENABLED %} <a href="{% url 'oidc_authentication_init' %}">Login with OpenID Connect </a> {% endif %} - + <br /><br /> - + {% if data.error %} <br/> <div class='centerbox-error'> {{ data.error }} </div> - + {% elif data.success %} <br/> <div class='centerbox-success'> {{data.success}} </div> - + {% else %} <br /> - + {% endif %} - + </p> - - </form> + + </form> </div> {% if not data.success %} <b>Forgot password?</b><br/> Just leave it empty to get a login link by email! {% endif %} - + <br /><br /><br /> </center> @@ -62,3 +62,4 @@ {% include "footer.html" %} + diff --git a/services/webapp/code/rosetta/core_app/templates/main.html b/services/webapp/code/rosetta/core_app/templates/main.html index eb1a989..0bb1686 100644 --- a/services/webapp/code/rosetta/core_app/templates/main.html +++ b/services/webapp/code/rosetta/core_app/templates/main.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html"%} @@ -12,7 +12,7 @@ <h2 style="margin-top:10px; margin-left:25px; margin-right:25px; font-weight:100; line-height: 30px;"><i>A container-centric Science Platform<br></i></h2> </div> </div> - + <div class="container"> <div class="dashboard"> <div class="span8 offset2" style="margin: 30px auto; max-width:800px"> @@ -21,9 +21,9 @@ <br/><br/> This is an empty installation. To load some demo data, run <code>rosetta/populate</code>. </div> - + </div> - </div> + </div> </header> {% endif %} @@ -31,3 +31,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/navigation.html b/services/webapp/code/rosetta/core_app/templates/navigation.html index ecc8fbf..25ab05e 100644 --- a/services/webapp/code/rosetta/core_app/templates/navigation.html +++ b/services/webapp/code/rosetta/core_app/templates/navigation.html @@ -4,9 +4,9 @@ <nav id="sidebar-wrapper"> <ul class="sidebar-nav"> - + <a id="menu-close" href="#" class="btn btn-light btn-lg pull-right toggle"><i class="fa fa-times"></i></a> - <a href="/main" class="btn btn-light btn-lg pull-right toggle"><i class="fa fa-home"></i></a> + <a href="/main" class="btn btn-light btn-lg pull-right toggle"><i class="fa fa-home"></i></a> <li class="sidebar-brand" style="margin-top:50px"> @@ -16,13 +16,13 @@ <hr style="margin-top:10px; margin-bottom:20px; margin-left:10px; margin-right:10px"> {% if user.is_authenticated %} - + <li> <a href="/software" onclick = $("#menu-close").click(); >Software</a> </li> <li> <a href="/computing" onclick = $("#menu-close").click(); >Computing</a> - </li> + </li> <li> <a href="/storage" onclick = $("#menu-close").click(); >Storage</a> @@ -34,30 +34,30 @@ <li> <a href="/account" onclick = $("#menu-close").click(); >Account</a> - </li> + </li> {% else %} - - - {% if LOCAL_AUTH_ENABLED %} + + + {% if LOCAL_AUTH_ENABLED %} <li> <center> <form class="form-signin" role="form" action='/login/' method='POST'> {% csrf_token %} - <input type="username" class="form-control" placeholder="Email" name='username' required> + <input type="username" class="form-control" placeholder="Email" name='username' required> <input type="password" class="form-control" placeholder="Password" name='password'> <input type='submit' class="btn btn-lg ha-btn-lg" value='Login' /> </form> - </center> + </center> </li> - + {% if OPENID_ENABLED %} <center><div style="margin-top:15px;margin-bottom:10px"><font color="#a9a9a9"> — OR —</font></div></center> - {% endif %} - + {% endif %} + {% endif %} <center> {% if OPENID_ENABLED %} - {% if LOCAL_AUTH_ENABLED %} + {% if LOCAL_AUTH_ENABLED %} <li style="padding-left:0; text-indent: 0"> <a href="{% url 'oidc_authentication_init' %}" style="padding-left:0; text-indent: 0">Login with OpenID Connect</a></li> {% else %} <li style="padding-left:0; text-indent: 0"> <a href="{% url 'oidc_authentication_init' %}" style="padding-left:0; text-indent: 0">Login</a></li> @@ -65,20 +65,21 @@ {% endif %} {% if LOCAL_AUTH_ENABLED %} <div style="padding:10px;"> - <font color="gray">Forgot password? Just leave it empty to get a login link by email. + <font color="gray">Forgot password? Just leave it empty to get a login link by email. Or, <a href="/register" style="color: #c0c0c0" onclick = $("#menu-close").click(); >Register</a>.</font> </div> {% endif %} </center> {% endif %} - + </ul> - - - - - - + + + + + + </nav> {% endif %} + 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 ec3536b..af0621e 100644 --- a/services/webapp/code/rosetta/core_app/templates/new_task.html +++ b/services/webapp/code/rosetta/core_app/templates/new_task.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,14 +6,14 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - <h1>New Task</h1> + <h1>New Task</h1> <hr> - <!-- {% if data.step == 'two' %} - <h3>Choose computing resource</h3> - {% elif data.step == 'three' %} - <h3>Task details and confirm</h3> + <!-- {% if data.step == 'two' %} + <h3>Choose computing resource</h3> + {% elif data.step == 'three' %} + <h3>Task details and confirm</h3> {% endif %} <br/> --> @@ -26,7 +26,7 @@ </div> </div> {% endif %} - + {% if data.step == 'two' %} @@ -49,8 +49,8 @@ {% include "components/computing.html" with computing=data.task_computing %} </div> </div> - - + + <!-- <div style="width:300px; float:left; border: #e0e0e0 solid 0px; margin:10px; background:#f8f8f8; margin-bottom:15px"> <table style="width:100%; height:126px; border: #e0e0e0 solid 1px; margin:0"> @@ -69,12 +69,12 @@ <h4>Details and confirm</h4> <div style="width:620px; float:left; border: #e0e0e0 solid 0px; margin-top:10px; ; margin-bottom:15px"> - - + + <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="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 }}" /> @@ -87,10 +87,10 @@ <td valign="top"><b>Task name</b></td> <td><input type="text" name="task_name" value="" placeholder="" size="37" required /></td> </tr> - + <tr> <td valign="top"> - {% if request.user.profile.is_power_user %} + {% if request.user.profile.is_power_user %} <b>Task password</b></td> {% else %} <b>Auth token</b></td> @@ -99,11 +99,11 @@ <input type="hidden" name="task_auth_token" value="{{data.task_auth_token}}"> - {% if request.user.profile.is_power_user %} + {% if request.user.profile.is_power_user %} <input type="text" name="task_auth_password" placeholder="{{data.task_auth_token}}" size="37" style="-webkit-text-security: disc;" /><br> <font size=-1> A one-time task password. By default set to a randomly generated token, and usually automatically handled by Rosetta. Six chars min. - </font> + </font> {% else %} <input type="text" name="task_auth_password_disabled" value="{{data.task_auth_token}}" size="37" readonly/><br> <font size=-1>A randomly generated token to be used as task password. Usually automatically handled by Rosetta when loggin-in to the task.</font> @@ -111,9 +111,9 @@ </td> </tr> - - - {% if request.user.profile.is_power_user %} + + + {% if request.user.profile.is_power_user %} <tr> <td><b>Access method</b></td><td> <select name="access_method" > @@ -138,7 +138,7 @@ </td> </tr> {% endif %} - + {% if data.task_computing.wms == 'slurm' %} <tr> <td><b>Computing options</b></td> @@ -151,14 +151,14 @@ </td> </tr> {% endif %} - + <tr><td colspan=2> - + </td></tr> </table> - - + + {% if data.task_computing.default_container_engine == 'singularity' and not data.task_container.supports_custom_interface_port %} <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> This container does not support custom interface ports and the computing resource you selected might use a container engine/runtime which does not support port mapping. In this case, if the container interface port is already allocated, the task will fail to start. @@ -176,21 +176,21 @@ <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 engine/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"> <i class="fa fa-exclamation-triangle" style="color:orange"></i> This container does not support configuring any authentication. This means that unless it is somehow built-in within the container, anyone will be able to access it. </p></div> {% endif %} --> - + <table style="max-width:620px;"><tr><td style="border: 1px solid lightgray;" > I understand that files saved or modified in the task, if not explicitly saved to a persistent storage, will be LOST upon task completition. </td><td style="border: 1px solid lightgray;" > <input class="form-check-input" type="checkbox" value="" id="invalidCheck" required> </td></table> - + <div style="width:640px; text-align:center; padding:20px"> - <input type="submit" value="Create task"> + <input type="submit" value="Create task"> </div> </form> </div> @@ -199,12 +199,12 @@ {% else %} Ok, task created. Go to your <a href="/tasks">task list</a>. - - {% endif %} - - + {% endif %} + + + </div> </div> </div> @@ -221,3 +221,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/page.html b/services/webapp/code/rosetta/core_app/templates/page.html index 9e13b28..c165dac 100644 --- a/services/webapp/code/rosetta/core_app/templates/page.html +++ b/services/webapp/code/rosetta/core_app/templates/page.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" with title=data.page.title %} {% include "navigation.html"%} {% include "logo.html" %} @@ -9,7 +9,7 @@ <!-- <h1>Example title</h1> <hr> --> {{ data.page.content | safe }} - + </div> </div> </div> @@ -24,3 +24,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/register.html b/services/webapp/code/rosetta/core_app/templates/register.html index 66bb4bf..9c137f5 100644 --- a/services/webapp/code/rosetta/core_app/templates/register.html +++ b/services/webapp/code/rosetta/core_app/templates/register.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,15 +6,15 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - + <h1>Register</h1> <hr> - - {% if data.status == "wrong_invite" %} + + {% if data.status == "wrong_invite" %} <div class='centerbox-error'> Sorry, this invitation code is not valid. </div> - + {% elif data.status == "already" %} <div class='centerbox'> You are already signed in, cannot create a new account. @@ -30,7 +30,7 @@ Ok, account created! </div> <br /> - + You signed up with the following email address: <b>{{data.user.email}}</b>. If this is wrong, change it now in your account settings. <br/> <br/> @@ -38,7 +38,7 @@ {% else %} <p><b>Welcome!</b></p> - + <p> {% if INVITATION_CODE_ENABLED %} An invitation code is required to register on the platform. If you have one, then you can sign up right now. Otherwise, please get in touch. @@ -46,10 +46,10 @@ Enter your email address and choose a password to register on the platform. {% endif %} </p> - + <br/> <br/> - + <div class='centerbox'> <form class="form-signin" role="form" action='/register/' method='POST'> {% csrf_token %} @@ -66,12 +66,12 @@ <center>Please not that we do not verify email addressed and passwords at this stage, so please <u>double check</u> the form above before signing up.</center> </div> --> {% endif %} - - {% if data.error %} + + {% if data.error %} <div class='centerbox-error'> {{ data.error }} </div> - + {% endif %} <br/> <br/> @@ -79,7 +79,7 @@ <br/> <br/> <br/> - + </div> </div> </div> @@ -90,3 +90,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/software.html b/services/webapp/code/rosetta/core_app/templates/software.html index fc69df7..c94a41e 100644 --- a/services/webapp/code/rosetta/core_app/templates/software.html +++ b/services/webapp/code/rosetta/core_app/templates/software.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,11 +6,11 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - + {% 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 %} + {% 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 %} @@ -18,62 +18,62 @@ <h1>New Task</h1> <hr/> <div style="padding-left:10px; padding-top:1px"><h4>Software container</h4></div> - <br/> + <br/> {% else %} <h1>Software containers</h1> <hr/> {% endif %} - - - + + + <div class="form-filter" style="margin-bottom:20px"> <form action="" method="POST"> - + <input type="hidden" name="mode" value="{{data.mode}}"> - + <input type="text" class="form-control" id="search_text" name="search_text" placeholder="Search..." style="width:200px; margin:0; display:inline" value="{{data.search_text}}" autofocus> <!-- onchange="this.form.submit()" --> <select class="form-control" id="search_owner" name="search_owner" style="width:120px; margin:0; display:inline"> - + {% if data.search_owner == 'All' %} <option selected>All</option> {% else %} - <option>All</option> + <option>All</option> {% endif %} - + {% if data.search_owner == 'Platform' %} <option selected>Platform</option> {% else %} - <option>Platform</option> + <option>Platform</option> {% endif %} - + {% if data.search_owner == 'User' %} <option selected>User</option> {% else %} - <option>User</option> + <option>User</option> {% endif %} - + </select> {% csrf_token %} <button type="submit" class="btn btn-secondary">Go</button> - + <font size=4.0em>|</font> <a href="/add_software">Add new...</a> - + </form> - - - + + + </div> - - + + {% endif %} - + <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%} @@ -82,10 +82,10 @@ {% for container in data.containers %} {% include "components/container.html" with container=container %} {% endfor %} - {% endif %} + {% endif %} {% endif %} </div> - + <br/> <br/> <br/> @@ -102,7 +102,7 @@ <script> $(document).ready(function() { - + var search_text_input = $("#search_text"); var len = search_text_input.val().length; search_text_input[0].focus(); @@ -113,3 +113,4 @@ $(document).ready(function() { + diff --git a/services/webapp/code/rosetta/core_app/templates/storage.html b/services/webapp/code/rosetta/core_app/templates/storage.html index f4a4c25..62ef047 100644 --- a/services/webapp/code/rosetta/core_app/templates/storage.html +++ b/services/webapp/code/rosetta/core_app/templates/storage.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,7 +6,7 @@ <div class="container" style="height:75%;"> <div class="dashboard" style="height:100%;"> <div class="span8 offset2" style="height:100%;"> - + <h1>Storage resources</h1> @@ -14,8 +14,8 @@ <iframe src="/static/RichFilemanager/index.html" style="overflow:hidden;height:100%;width:100%; border: 1px solid #c0c0c0" height="100%" width="100%"></iframe> - - + + </div> </div> </div> @@ -23,3 +23,4 @@ {% include "footer.html" %} + diff --git a/services/webapp/code/rosetta/core_app/templates/success.html b/services/webapp/code/rosetta/core_app/templates/success.html index 44ef368..728ec4e 100644 --- a/services/webapp/code/rosetta/core_app/templates/success.html +++ b/services/webapp/code/rosetta/core_app/templates/success.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -9,13 +9,13 @@ <h1>Success</h1> <hr> <br/> - <div class='centerbox-success-outer'> - <span class='centerbox-success-inner'> + <div class='centerbox-success-outer'> + <span class='centerbox-success-inner'> {{data.success}} </span> </div> - - + + <br/> <br/> @@ -23,7 +23,7 @@ <br/> <br/> <br/> - + </div> </div> </div> @@ -34,3 +34,4 @@ + diff --git a/services/webapp/code/rosetta/core_app/templates/task_connect.html b/services/webapp/code/rosetta/core_app/templates/task_connect.html index c1ba20b..ac276f8 100644 --- a/services/webapp/code/rosetta/core_app/templates/task_connect.html +++ b/services/webapp/code/rosetta/core_app/templates/task_connect.html @@ -1,9 +1,9 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html"%} <!-- with body_args="style='background: #202020'" --> <center> - + <div style="width:370px;"> <form class="form-signin" role="form" action='/direct_connect/{{data.task.uuid}}/' method='POST'> {% csrf_token %} @@ -14,7 +14,7 @@ <h2> Connecting to task <b>{{ data.task.name }}</b></h2> <p style="font-size: 16px;"> <br /> - + {% if not data.task.interface_status == 'running' %} <br/> <div class="alert alert-warning" role="alert"><i class="fa fa-warning"></i> the task interface is not up, cannot connect.</div> @@ -22,20 +22,20 @@ <br/><br/> <i>Note: if you just launched the task, this alert might be due to the normal task startup time.</i> {% else %} - + {% if not data.task.requires_proxy_auth %} {% if data.task.container.interface_auth_user %} - User: <input style="margin-bottom:15px;" type="username" class="form-control" value="{{ data.task.container.interface_auth_user }}"name='username' readonly > + User: <input style="margin-bottom:15px;" type="username" class="form-control" value="{{ data.task.container.interface_auth_user }}"name='username' readonly > {% else %} - + {% endif %} {% else %} User: <input style="margin-bottom:15px;" type="username" class="form-control" value="{{ request.user.email }}"name='username' readonly > {% endif %} - - + + {% if data.task.requires_proxy_auth %} - + {% if data.task.auth_token %} Password: <input type="text" size=37 class="form-control" placeholder="" value="{{data.task.auth_token}}" name='password' readonly> {% if data.task.container.disable_http_basicauth_embedding %} @@ -49,11 +49,11 @@ <p style="margin-left:10px; font-size:0.9em; color:#484848"> <i class="fa fa-info-circle" style="color:#337ab7"></i> This task will require to manually enter the password set up when creating the task - </p> + </p> {% endif %} - + {% else %} - + {% if data.task.auth_token %} Password: <input type="text" class="form-control" placeholder="" value="{{data.task.auth_token}}" name='password' readonly> <p style="margin-left:10px; margin-top:30px; font-size:0.9em; color:#484848"> @@ -61,9 +61,9 @@ {% if not data.task.requires_proxy_auth and data.task.container.interface_auth_user %} This task will require to manually enter the above user and password token {% else %} - This task will require to manually enter the above password token + This task will require to manually enter the above password token {% endif %} - </p> + </p> {% else %} Password: <input type="password" class="form-control" placeholder="" value="placeholder" name='password' readonly> <p style="margin-left:10px; margin-top:30px; font-size:0.9em; color:#484848"> @@ -71,17 +71,17 @@ {% if not data.task.requires_proxy_auth and data.task.container.interface_auth_user %} This task will require to manually enter the above user and the password set up when creating the task {% else %} - This task will require to manually enter the password set up when creating the task + This task will require to manually enter the password set up when creating the task {% endif %} - </p> + </p> {% endif %} - + {% endif %} - + <br /> - - - {% if data.task.container.interface_protocol == 'http' or data.task.container.interface_protocol == 'https' %} + + + {% if data.task.container.interface_protocol == 'http' or data.task.container.interface_protocol == 'https' %} <input type='submit' style="width:110px" class="btn btn-lg btn-success btn-block" value='Log in' /> {% else %} To connect, use a program suitable for this task interface protocol ({{data.task.container.interface_protocol}}) with:<br /> @@ -92,8 +92,8 @@ {% endif %} {% endif%} </p> - - </form> + + </form> </div> <br /><br /> {% if data.task.interface_status == 'running' %} @@ -101,7 +101,7 @@ <p style="margin-left:10px; font-size:0.9em; color:rgb(200,200,200); max-width:600px"> <i class="fa fa-info-circle"></i> Some web browsers (i.e. Safari) might require to manually re-enter the above user credentials. You can copy-paste them, or switch - to a web browser which supports embedding user credentials in the connection URL (as Chorme, Edge or Firefox). + to a web browser which supports embedding user credentials in the connection URL (as Chorme, Edge or Firefox). </p> {% endif %} {% endif %} @@ -112,3 +112,4 @@ {% include "footer.html" %} + diff --git a/services/webapp/code/rosetta/core_app/templates/task_log.html b/services/webapp/code/rosetta/core_app/templates/task_log.html index bc4e0ad..1ee47bf 100644 --- a/services/webapp/code/rosetta/core_app/templates/task_log.html +++ b/services/webapp/code/rosetta/core_app/templates/task_log.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" with refresh=data.refresh %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,20 +6,20 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - - <h1><a href="/tasks">Tasks</a> <span style="font-size:18px"> / <a href="/tasks/?uuid={{data.task.uuid}}">{{ data.task.name }}</a> / Log</span></h1> - + + <h1><a href="/tasks">Tasks</a> <span style="font-size:18px"> / <a href="/tasks/?uuid={{data.task.uuid}}">{{ data.task.name }}</a> / Log</span></h1> + <hr> - - <b>UUID:</b> {{ data.task.uuid }} - <b>Status:</b> {{ data.task.status }} + + <b>UUID:</b> {{ data.task.uuid }} + <b>Status:</b> {{ data.task.status }} <b>Auto refresh:{{data.refresh}}</b> - {% if not data.refresh %} OFF {% else %} <a href="?uuid={{data.task.uuid}}">OFF</a> {% endif %} | - {% if data.refresh == "3" %} 3s {% else %} <a href="?uuid={{data.task.uuid}}&refresh=3">3s</a> {% endif %} | - {% if data.refresh == "10" %} 10s{% else %} <a href="?uuid={{data.task.uuid}}}&refresh=10">10s</a> {% endif %} | - {% if data.refresh == "60" %} 60s{% else %} <a href="?uuid={{data.task.uuid}}}&refresh=60">60s</a> {% endif %} - + {% if not data.refresh %} OFF {% else %} <a href="?uuid={{data.task.uuid}}">OFF</a> {% endif %} | + {% if data.refresh == "3" %} 3s {% else %} <a href="?uuid={{data.task.uuid}}&refresh=3">3s</a> {% endif %} | + {% if data.refresh == "10" %} 10s{% else %} <a href="?uuid={{data.task.uuid}}}&refresh=10">10s</a> {% endif %} | + {% if data.refresh == "60" %} 60s{% else %} <a href="?uuid={{data.task.uuid}}}&refresh=60">60s</a> {% endif %} + <pre id="output" style="border: 1px solid #a0a0a0; width: 100%; height: 500px; background-color:black; color:white; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: white-space: -o-pre-wrap; word-wrap: break-word;">{{ data.log }}</pre> @@ -29,7 +29,7 @@ <br/> <br/> <br/> - + </div> </div> </div> @@ -47,3 +47,4 @@ jQuery( function(){ + diff --git a/services/webapp/code/rosetta/core_app/templates/tasks.html b/services/webapp/code/rosetta/core_app/templates/tasks.html index 11ea3ad..22a79d1 100644 --- a/services/webapp/code/rosetta/core_app/templates/tasks.html +++ b/services/webapp/code/rosetta/core_app/templates/tasks.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static %} {% include "header.html" %} {% include "navigation.html" %} {% include "logo.html" %} @@ -6,9 +6,9 @@ <div class="container"> <div class="dashboard"> <div class="span8 offset2"> - + {% if data.task %} - <h1><a href="/tasks">Tasks</a> <span style="font-size:18px"> / {{ data.task.name }}</span></h1> + <h1><a href="/tasks">Tasks</a> <span style="font-size:18px"> / {{ data.task.name }}</span></h1> {% else %} <h1>Tasks</h1> {% endif %} @@ -23,7 +23,7 @@ {% endfor %} {% endif %} </div> - + {% if not data.task %} <div class="row" style="padding:10px; padding-left:15px"> <a href="/new_task">New task...</a> @@ -36,7 +36,7 @@ <br/> <br/> <br/> - + </div> </div> </div> @@ -53,7 +53,7 @@ function toggle_visibility(id) { x.style.display = "block"; y.style.display = "none" z.style.display= "inline" - + } else { x.style.display = "none"; y.style.display= "inline" @@ -63,3 +63,4 @@ function toggle_visibility(id) { </script> + diff --git a/services/webapp/code/rosetta/core_app/tests/common.py b/services/webapp/code/rosetta/core_app/tests/common.py index 75a1a7e..88839cb 100644 --- a/services/webapp/code/rosetta/core_app/tests/common.py +++ b/services/webapp/code/rosetta/core_app/tests/common.py @@ -26,14 +26,14 @@ class BaseAPITestCase(TestCase): request_func = getattr(self.client, request_method) status_code = None - + if 'multipart' in kwargs and kwargs['multipart'] is True: - # Do nothing, this is a "special", multipart request + # Do nothing, this is a "special", multipart request pass else: if 'content_type' not in kwargs and request_method != 'get': kwargs['content_type'] = 'application/json' - + if 'data' in kwargs and request_method != 'get' and kwargs['content_type'] == 'application/json': data = kwargs.get('data', '') kwargs['data'] = json.dumps(data) @@ -54,10 +54,10 @@ class BaseAPITestCase(TestCase): self.response.content_dict = json.loads(self.response.content) else: self.response.content_dict = {} - + except: self.response.content_dict = {} - + if status_code: if not self.response.status_code == status_code: raise Exception('Error with response:' + str(self.response)) @@ -91,3 +91,4 @@ class BaseAPITestCase(TestCase): + diff --git a/services/webapp/code/rosetta/core_app/tests/test_apis.py b/services/webapp/code/rosetta/core_app/tests/test_apis.py index 7ef1ed7..530d9a6 100644 --- a/services/webapp/code/rosetta/core_app/tests/test_apis.py +++ b/services/webapp/code/rosetta/core_app/tests/test_apis.py @@ -1,14 +1,14 @@ import json from django.contrib.auth.models import User - + from .common import BaseAPITestCase from ..models import Profile class ApiTests(BaseAPITestCase): def setUp(self): - + # Create test users self.user = User.objects.create_user('testuser', password='testpass') self.anotheruser = User.objects.create_user('anotheruser', password='anotherpass') @@ -18,8 +18,8 @@ class ApiTests(BaseAPITestCase): def test_api_web_auth(self): - '''Test auth using login api''' - + '''Test auth using login api''' + # No user at all resp = self.post('/api/v1/base/login/', data={}) self.assertEqual(resp.status_code, 401) @@ -46,4 +46,4 @@ class ApiTests(BaseAPITestCase): - \ No newline at end of file + diff --git a/services/webapp/code/rosetta/core_app/tests/test_models.py b/services/webapp/code/rosetta/core_app/tests/test_models.py index 5fc304a..1e1da97 100644 --- a/services/webapp/code/rosetta/core_app/tests/test_models.py +++ b/services/webapp/code/rosetta/core_app/tests/test_models.py @@ -1,14 +1,14 @@ import json from django.contrib.auth.models import User - + from .common import BaseAPITestCase from ..models import Profile, Computing class Modeltest(BaseAPITestCase): def setUp(self): - + # Create test users self.user = User.objects.create_user('testuser', password='testpass') self.anotheruser = User.objects.create_user('anotheruser', password='anotherpass') @@ -18,9 +18,10 @@ class Modeltest(BaseAPITestCase): def test_computing(self): - '''Test Computing and their Conf models''' - + '''Test Computing and their Conf models''' + computing = Computing.objects.create(name='MyComp', type='remote') - - + + + diff --git a/services/webapp/code/rosetta/core_app/tests/test_utils.py b/services/webapp/code/rosetta/core_app/tests/test_utils.py index f48fbde..380e2c2 100644 --- a/services/webapp/code/rosetta/core_app/tests/test_utils.py +++ b/services/webapp/code/rosetta/core_app/tests/test_utils.py @@ -1,7 +1,7 @@ import json from django.contrib.auth.models import User - + from .common import BaseAPITestCase from ..utils import sanitize_container_env_vars @@ -11,8 +11,8 @@ class TestUtils(BaseAPITestCase): pass def test_sanitize_user_env_vars(self): - '''Test sanitize use env vars''' - + '''Test sanitize use env vars''' + # Basic env_vars = {'myvar': 'a'} self.assertEqual(sanitize_container_env_vars(env_vars),env_vars) @@ -20,15 +20,16 @@ class TestUtils(BaseAPITestCase): # Allowed specia env_vars = {'myvar': '/a_directory/a-test'} self.assertEqual(sanitize_container_env_vars(env_vars),env_vars) - + # Potential malicious env_vars = {'myvar': '$(rm -rf)'} with self.assertRaises(ValueError): sanitize_container_env_vars(env_vars) - - - - - - + + + + + + + diff --git a/services/webapp/code/rosetta/core_app/utils.py b/services/webapp/code/rosetta/core_app/utils.py index ae57126..dc4a44c 100644 --- a/services/webapp/code/rosetta/core_app/utils.py +++ b/services/webapp/code/rosetta/core_app/utils.py @@ -45,11 +45,11 @@ def booleanize(*args, **kwargs): break else: raise Exception('Internal Error') - + # Handle shortcut: an arg with its name equal to ist value is considered as True if name==value: return True - + if isinstance(value, bool): return value else: @@ -63,7 +63,7 @@ def send_email(to, subject, text): # Importing here instead of on top avoids circular dependencies problems when loading booleanize in settings from django.conf import settings - + if settings.DJANGO_EMAIL_SERVICE == 'Sendgrid': import sendgrid from sendgrid.helpers.mail import Email,Content,Mail @@ -74,7 +74,7 @@ def send_email(to, subject, text): subject = subject content = Content('text/plain', text) mail = Mail(from_email, subject, to_email, content) - + try: response = sg.client.mail.send.post(request_body=mail.get()) @@ -83,17 +83,17 @@ def send_email(to, subject, text): #logger.debug(response.headers) except Exception as e: logger.error(e) - + #logger.debug(response) - + def format_exception(e, debug=False): - + # Importing here instead of on top avoids circular dependencies problems when loading booleanize in settings from django.conf import settings if settings.DEBUG: - # Cutting away the last char removed the newline at the end of the stacktrace + # Cutting away the last char removed the newline at the end of the stacktrace return str('Got exception "{}" of type "{}" with traceback:\n{}'.format(e.__class__.__name__, type(e), traceback.format_exc()))[:-1] else: return str('Got exception "{}" of type "{}" with traceback "{}"'.format(e.__class__.__name__, type(e), traceback.format_exc().replace('\n', '|'))) @@ -106,7 +106,7 @@ def log_user_activity(level, msg, request, caller=None): #caller = inspect.stack()[1][3] #if caller == "post": # caller = inspect.stack()[2][3] - + try: msg = str(caller) + " view - USER " + str(request.user.email) + ": " + str(msg) except AttributeError: @@ -116,12 +116,12 @@ def log_user_activity(level, msg, request, caller=None): level = getattr(logging, level) except: raise - + logger.log(level, msg) def username_hash(email): - '''Create md5 base 64 (25 chrars) hash from user email:''' + '''Create md5 base 64 (25 chrars) hash from user email:''' m = hashlib.md5() m.update(email) username = m.hexdigest().decode('hex').encode('base64')[:-3] @@ -129,7 +129,7 @@ def username_hash(email): def random_username(): - '''Create a random string of 156 chars to be used as username''' + '''Create a random string of 156 chars to be used as username''' username = ''.join(random.choice('abcdefghilmnopqrtuvz') for _ in range(16)) return username @@ -155,27 +155,27 @@ def finalize_user_creation(user, auth='local'): if not out.exit_code == 0: logger.error(out) raise ErrorMessage('Something went wrong in creating user keys folder. Please contact support') - - command= "/bin/bash -c \"ssh-keygen -q -t rsa -N '' -C {}@rosetta -f /data/resources/keys/{}_id_rsa 2>/dev/null <<< y >/dev/null\"".format(user.email.split('@')[0], user.username) + + command= "/bin/bash -c \"ssh-keygen -q -t rsa -N '' -C {}@rosetta -f /data/resources/keys/{}_id_rsa 2>/dev/null <<< y >/dev/null\"".format(user.email.split('@')[0], user.username) out = os_shell(command, capture=True) if not out.exit_code == 0: logger.error(out) raise ErrorMessage('Something went wrong in creating user keys. Please contact support') - - + + # Create key objects KeyPair.objects.create(user = user, default = True, private_key_file = '/data/resources/keys/{}_id_rsa'.format(user.username), public_key_file = '/data/resources/keys/{}_id_rsa.pub'.format(user.username)) - + def sanitize_shell_encoding(text): return text.encode("utf-8", errors="ignore") def format_shell_error(stdout, stderr, exit_code): - + string = '\n#---------------------------------' string += '\n# Shell exited with exit code {}'.format(exit_code) string += '\n#---------------------------------\n' @@ -193,14 +193,14 @@ def format_shell_error(stdout, stderr, exit_code): def os_shell(command, capture=False, verbose=False, interactive=False, silent=False): '''Execute a command in the OS shell. By default prints everything. If the capture switch is set, then it returns a namedtuple with stdout, stderr, and exit code.''' - + if capture and verbose: raise Exception('You cannot ask at the same time for capture and verbose, sorry') # Log command logger.debug('Shell executing command: "%s"', command) - # Execute command in interactive mode + # Execute command in interactive mode if verbose or interactive: exit_code = subprocess.call(command, shell=True) if exit_code == 0: @@ -210,7 +210,7 @@ def os_shell(command, capture=False, verbose=False, interactive=False, silent=Fa # Execute command getting stdout and stderr # http://www.saltycrane.com/blog/2008/09/how-get-stdout-and-stderr-using-python-subprocess-module/ - + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) (stdout, stderr) = process.communicate() exit_code = process.wait() @@ -230,8 +230,8 @@ def os_shell(command, capture=False, verbose=False, interactive=False, silent=Fa if capture: return Output(stdout, stderr, exit_code) else: - print(format_shell_error(stdout, stderr, exit_code)) - return False + print(format_shell_error(stdout, stderr, exit_code)) + return False else: if capture: return Output(stdout, stderr, exit_code) @@ -247,16 +247,16 @@ def os_shell(command, capture=False, verbose=False, interactive=False, silent=Fa def get_md5(string): if not string: raise Exception("Colund not compute md5 of empty/None value") - + m = hashlib.md5() - + # Fix for Python3 try: if isinstance(string,unicode): string=string.encode('utf-8') except NameError: string=string.encode('utf-8') - + m.update(string) md5 = str(m.hexdigest()) return md5 @@ -264,14 +264,14 @@ def get_md5(string): def timezonize(timezone): '''Convert a string representation of a timezone to its pytz object or do nothing if the argument is already a pytz timezone''' - + # Check if timezone is a valid pytz object is hard as it seems that they are spread arount the pytz package. # Option 1): Try to convert if string or unicode, else try to - # instantiate a datetiem object with the timezone to see if it is valid + # instantiate a datetiem object with the timezone to see if it is valid # Option 2): Get all memebers of the pytz package and check for type, see # http://stackoverflow.com/questions/14570802/python-check-if-object-is-instance-of-any-class-from-a-certain-module # Option 3) perform a hand.made test. We go for this one, tests would fail if it gets broken - + if not 'pytz' in str(type(timezone)): timezone = pytz.timezone(timezone) return timezone @@ -296,25 +296,25 @@ def now_dt(tzinfo='UTC'): def dt(*args, **kwargs): '''Initialize a datetime object in the proper way. Using the standard datetime leads to a lot of problems with the tz package. Also, it forces UTC timezone if no timezone is specified''' - + if 'tz' in kwargs: tzinfo = kwargs.pop('tz') else: tzinfo = kwargs.pop('tzinfo', None) - - offset_s = kwargs.pop('offset_s', None) + + offset_s = kwargs.pop('offset_s', None) trustme = kwargs.pop('trustme', None) - + if kwargs: raise Exception('Unhandled arg: "{}".'.format(kwargs)) - + if (tzinfo is None): # Force UTC if None timezone = timezonize('UTC') - + else: timezone = timezonize(tzinfo) - + if offset_s: # Special case for the offset if not tzoffset: @@ -324,7 +324,7 @@ def dt(*args, **kwargs): # Standard timezone time_dt = timezone.localize(datetime.datetime(*args)) - # Check consistency + # Check consistency if not trustme and timezone != pytz.UTC: if not check_dt_consistency(time_dt): raise Exception('Sorry, time {} does not exists on timezone {}'.format(time_dt, timezone)) @@ -342,11 +342,11 @@ def check_dt_consistency(date_dt): # https://en.wikipedia.org/wiki/Tz_database # https://www.iana.org/time-zones - + if date_dt.tzinfo is None: return True else: - + # This check is quite heavy but there is apparently no other way to do it. if date_dt.utcoffset() != dt_from_s(s_from_dt(date_dt), tz=date_dt.tzinfo).utcoffset(): return False @@ -396,7 +396,7 @@ def dt_from_s(timestamp_s, tz=None): pytz_tz = timezonize(tz) timestamp_dt = timestamp_dt.replace(tzinfo=pytz.utc).astimezone(pytz_tz) - + return timestamp_dt @@ -414,38 +414,38 @@ def dt_from_str(string, timezone=None): # 1) YYYY-MM-DDThh:mm:ssZ # 2) YYYY-MM-DDThh:mm:ss.{u}Z - # Supported formats with offset + # Supported formats with offset # 3) YYYY-MM-DDThh:mm:ss+ZZ:ZZ # 4) YYYY-MM-DDThh:mm:ss.{u}+ZZ:ZZ # Split and parse standard part date, time = string.split('T') - + if time.endswith('Z'): # UTC offset_s = 0 time = time[:-1] - + elif ('+') in time: # Positive offset time, offset = time.split('+') # Set time and extract positive offset - offset_s = (int(offset.split(':')[0])*60 + int(offset.split(':')[1]) )* 60 - + offset_s = (int(offset.split(':')[0])*60 + int(offset.split(':')[1]) )* 60 + elif ('-') in time: # Negative offset time, offset = time.split('-') # Set time and extract negative offset - offset_s = -1 * (int(offset.split(':')[0])*60 + int(offset.split(':')[1])) * 60 - + offset_s = -1 * (int(offset.split(':')[0])*60 + int(offset.split(':')[1])) * 60 + else: raise Exception('Format error') - + # Handle time hour, minute, second = time.split(':') - + # Now parse date (easy) - year, month, day = date.split('-') + year, month, day = date.split('-') # Convert everything to int year = int(year) @@ -459,7 +459,7 @@ def dt_from_str(string, timezone=None): else: second = int(second) usecond = 0 - + return dt(year, month, day, hour, minute, second, usecond, offset_s=offset_s) @@ -524,7 +524,7 @@ def get_platform_registry(): platform_registry_conn_string = '{}:{}'.format(platform_registry_host, platform_registry_port) return platform_registry_conn_string - + def get_rosetta_tasks_tunnel_host(): # Importing here instead of on top avoids circular dependencies problems when loading booleanize in settings from django.conf import settings @@ -544,9 +544,9 @@ def hash_string_to_int(string): def get_ssh_access_mode_credentials(computing, user): - + from .models import KeyPair - + # Get computing host try: computing_host = computing.conf.get('host') @@ -562,7 +562,7 @@ def get_ssh_access_mode_credentials(computing, user): computing_port = 22 if not computing_port: computing_port = 22 - + # Get computing user and keys if computing.auth_mode == 'user_keys': computing_user = user.profile.get_extra_conf('computing_user', computing) @@ -570,7 +570,7 @@ def get_ssh_access_mode_credentials(computing, user): raise ValueError('No \'computing_user\' parameter found for computing resource \'{}\' in user profile'.format(computing.name)) # Get user key computing_keys = KeyPair.objects.get(user=user, default=True) - elif computing.auth_mode == 'platform_keys': + elif computing.auth_mode == 'platform_keys': computing_user = computing.conf.get('user') computing_keys = KeyPair.objects.get(user=None, default=True) else: @@ -584,7 +584,7 @@ def setup_tunnel_and_proxy(task): # Importing here instead of on top avoids circular dependencies problems when loading booleanize in settings from .models import Task, KeyPair, TaskStatuses - + # If there is no tunnel port allocated yet, find one if not task.tcp_tunnel_port: @@ -629,8 +629,8 @@ def setup_tunnel_and_proxy(task): user_keys = KeyPair.objects.get(user=task.user, default=True) # Tunnel command - if task.computing.type == 'remotehop': - + if task.computing.type == 'remotehop': + # Get computing params first_host = task.computing.conf.get('first_host') first_user = task.computing.conf.get('first_user') @@ -638,18 +638,18 @@ def setup_tunnel_and_proxy(task): #second_user = task.computing.conf.get('second_user') #setup_command = task.computing.conf.get('setup_command') #base_port = task.computing.conf.get('base_port') - + tunnel_command= 'ssh -4 -i {} -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} {}@{} & '.format(user_keys.private_key_file, task.tcp_tunnel_port, task.interface_ip, task.interface_port, first_user, first_host) else: - + if task.computing.access_mode.startswith('ssh'): computing_user, computing_host, computing_port, computing_keys = get_ssh_access_mode_credentials(task.computing, task.user) tunnel_command = 'ssh -p {} -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no -o ConnectTimeout=10 '.format(computing_port, computing_keys.private_key_file) tunnel_command += '-nNT -L 0.0.0.0:{}:{}:{} {}@{}'.format(task.tcp_tunnel_port, task.interface_ip, task.interface_port, computing_user, computing_host) else: tunnel_command= 'ssh -4 -o StrictHostKeyChecking=no -nNT -L 0.0.0.0:{}:{}:{} localhost & '.format(task.tcp_tunnel_port, task.interface_ip, task.interface_port) - + background_tunnel_command = 'nohup {} >/dev/null 2>&1 &'.format(tunnel_command) # Log @@ -658,20 +658,20 @@ def setup_tunnel_and_proxy(task): # Execute subprocess.Popen(background_tunnel_command, shell=True) - + # Setup the proxy now (if required.) if task.requires_proxy: - + # Ensure conf directory exists if not os.path.exists('/shared/etc_apache2_sites_enabled'): os.makedirs('/shared/etc_apache2_sites_enabled') - + # Set conf file name apache_conf_file = '/shared/etc_apache2_sites_enabled/{}.conf'.format(task.uuid) - - # Check if proxy conf exists + + # Check if proxy conf exists if not os.path.exists(apache_conf_file): - + # Write conf file # Some info about the various SSL switches: https://serverfault.com/questions/577616/using-https-between-apache-loadbalancer-and-backends logger.debug('Writing task proxy conf to {}'.format(apache_conf_file)) @@ -679,7 +679,7 @@ def setup_tunnel_and_proxy(task): rosetta_tasks_proxy_host = get_rosetta_tasks_proxy_host() apache_conf_content = ''' #--------------------------- -# Task interface proxy +# Task interface proxy #--------------------------- Listen '''+str(task.tcp_tunnel_port)+''' @@ -694,19 +694,19 @@ Listen '''+str(task.tcp_tunnel_port)+''' </VirtualHost> <VirtualHost *:'''+str(task.tcp_tunnel_port)+'''> - + ServerName '''+rosetta_tasks_proxy_host+''' ServerAdmin admin@rosetta.platform - + SSLEngine on SSLCertificateFile /etc/letsencrypt/live/'''+rosetta_tasks_proxy_host+'''/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/'''+rosetta_tasks_proxy_host+'''/privkey.pem SSLCACertificateFile /etc/letsencrypt/live/'''+rosetta_tasks_proxy_host+'''/fullchain.pem - + SSLProxyEngine On - SSLProxyVerify none + SSLProxyVerify none SSLProxyCheckPeerCN off - SSLProxyCheckPeerName off + SSLProxyCheckPeerName off BrowserMatch "MSIE [2-6]" \ nokeepalive ssl-unclean-shutdown \ @@ -723,7 +723,7 @@ Listen '''+str(task.tcp_tunnel_port)+''' AuthType Basic AuthName "Restricted area" AuthUserFile /shared/etc_apache2_sites_enabled/'''+str(task.uuid)+'''.htpasswd - Require valid-user + Require valid-user # preserve Host header to avoid cross-origin problems ProxyPreserveHost on @@ -737,32 +737,32 @@ Listen '''+str(task.tcp_tunnel_port)+''' ''' with open(apache_conf_file, 'w') as f: f.write(apache_conf_content) - + # Now check if conf exist on proxy logger.debug('Checking if conf is enabled on proxy service') out = os_shell('ssh -o StrictHostKeyChecking=no proxy "[ -e /etc/apache2/sites-enabled/{}.conf ]"'.format(task.uuid), capture=True) - + if out.exit_code == 1: - + logger.debug('Conf not enabled on proxy service, linkig it and reloading Apache conf') - + # Link on proxy since conf does not exist out = os_shell('ssh -o StrictHostKeyChecking=no proxy "sudo ln -s /shared/etc_apache2_sites_enabled/{0}.conf /etc/apache2/sites-enabled/{0}.conf"'.format(task.uuid), capture=True) if out.exit_code != 0: logger.error(out.stderr) - raise ErrorMessage('Something went wrong when activating the task proxy conf') - + raise ErrorMessage('Something went wrong when activating the task proxy conf') + # Reload apache conf on Proxy out = os_shell('ssh -o StrictHostKeyChecking=no proxy "sudo apache2ctl graceful"', capture=True) if out.exit_code != 0: - logger.error(out.stderr) - raise ErrorMessage('Something went wrong when loading the task proxy conf') - + logger.error(out.stderr) + raise ErrorMessage('Something went wrong when loading the task proxy conf') + def sanitize_container_env_vars(env_vars): - + for env_var in env_vars: - + # Check only alphanumeric chars, slashed, dashes and underscores if not re.match("^[/A-Za-z0-9_-]*$", env_vars[env_var]): raise ValueError('Value "{}" for env var "{}" is not valid: only alphanumeric, slashes, dashes and underscores are.'.format(env_vars[env_var], env_var)) @@ -770,9 +770,9 @@ def sanitize_container_env_vars(env_vars): return env_vars -def get_or_create_container_from_repository(user, repository_url, repository_tag=None, container_name=None, container_description=None): - - from .models import Container +def get_or_create_container_from_repository(user, repository_url, repository_tag=None, container_name=None, container_description=None): + + from .models import Container logger.debug('Called get_or_create_container_from_repository with repository_url="{}" and repository_tag="{}"'.format(repository_url,repository_tag)) # Set repo name @@ -781,13 +781,13 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag # If building: #{"message": "Successfully built 5a2089b2c334\n", "phase": "building"} #{"message": "Successfully tagged r2dhttps-3a-2f-2fgithub-2ecom-2fnorvig-2fpytudes5e745c3:latest\n", "phase": "building"} - + # If reusing: #{"message": "Reusing existing image (r2dhttps-3a-2f-2fgithub-2ecom-2fnorvig-2fpytudes5e745c3), not building."} - + appendix = 'CMD ["jupyter", "notebook", "--ip", "0.0.0.0", "--NotebookApp.token", ""]' - + # Build the Docker container for this repo if repository_tag: command = 'sudo jupyter-repo2docker --ref {} --user-id 1000 --user-name jovyan --no-run --appendix \'{}\' --json-logs {}'.format(repository_tag, appendix, repository_url) @@ -796,7 +796,7 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag out = os_shell(command, capture=True) if out.exit_code != 0: logger.error(out.stderr) - raise ErrorMessage('Something went wrong when creating the Dockerfile for repository "{}"'.format(repository_url)) + raise ErrorMessage('Something went wrong when creating the Dockerfile for repository "{}"'.format(repository_url)) # Convert output to lines out_lines = out.stderr.split('\n') @@ -818,7 +818,7 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag image_name_for_tag = repo2docker_image_name.replace(':latest','') else: image_name_for_tag = repo2docker_image_name - + image_tag = image_name_for_tag[-7:] # The last part of the image name generated by repo2docker is the git short hash # Re-tag image taking into account that if we are using the proxy as registry we use localhost or it won't work @@ -826,17 +826,17 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag push_registry = 'localhost:5000' else: push_registry = registry - + out = os_shell('sudo docker tag {} {}/{}:{}'.format(repo2docker_image_name,push_registry,image_name,image_tag) , capture=True) if out.exit_code != 0: logger.error(out.stderr) - raise ErrorMessage('Something went wrong when tagging the container for repository "{}"'.format(repository_url)) + raise ErrorMessage('Something went wrong when tagging the container for repository "{}"'.format(repository_url)) # Push image to the (local) registry out = os_shell('sudo docker push {}/{}:{}'.format(push_registry,image_name,image_tag) , capture=True) if out.exit_code != 0: logger.error(out.stderr) - raise ErrorMessage('Something went wrong when pushing the container for repository "{}"'.format(repository_url)) + raise ErrorMessage('Something went wrong when pushing the container for repository "{}"'.format(repository_url)) # Create the container if not already existent try: @@ -858,11 +858,11 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag repository_description_from_source += ' Built from {}'.format(repository_url) except: pass - - # Set default container name and description + + # Set default container name and description if not container_name: container_name = repository_name_from_source if repository_name_from_source else repository_name - + if not container_description: container_description = repository_description_from_source if repository_description_from_source else 'Built from {}'.format(repository_url) @@ -882,3 +882,4 @@ def get_or_create_container_from_repository(user, repository_url, repository_tag supports_interface_auth = False) return container + diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py index c74f08c..ff90b2f 100644 --- a/services/webapp/code/rosetta/core_app/views.py +++ b/services/webapp/code/rosetta/core_app/views.py @@ -38,7 +38,7 @@ _task_cache = {} def login_view(request): data = {} - + # Set post login page post_login_page = request.COOKIES.get('post_login_redirect') if post_login_page is None: @@ -93,9 +93,9 @@ def login_view(request): if user.profile.auth == 'local': logger.debug('Sending login token via mail to {}'.format(user.email)) - + token = uuid.uuid4() - + # Create token or update if existent (and never used) try: loginToken = LoginToken.objects.get(user=user) @@ -109,7 +109,7 @@ def login_view(request): except Exception as e: logger.error(format_exception(e)) raise ErrorMessage('Something went wrong. Please retry later.') - + # Return here, we don't want to give any hints about existing users data['success'] = 'Ok, if we have your data you will receive a login link by email shortly.' return render(request, 'success.html', {'data': data}) @@ -178,20 +178,20 @@ def register_view(request): email = request.POST.get('email') password = request.POST.get('password') invitation = request.POST.get('invitation') - + if settings.INVITATION_CODE: if invitation != settings.INVITATION_CODE: raise ErrorMessage('Wrong invitation code') if '@' not in email: raise ErrorMessage('Detected invalid email address') - + # Register the user user = User.objects.create_user(random_username(), password=password, email=email) # Is this necessary? user.save() - + data['user'] = user finalize_user_creation(user) @@ -199,7 +199,7 @@ def register_view(request): # Manually set the auth backend for the user user.backend = 'django.contrib.auth.backends.ModelBackend' login(request, user) - + data['status'] = 'activated' # All other cases, render the login page again with no other data than title @@ -218,7 +218,7 @@ def main_view(request): # Init data data = {} - + # Get custom home page if any try: page = Page.objects.get(id='main') @@ -234,7 +234,7 @@ def page_view(request, page_id): # Init data data = {} - + # Get the page try: page = Page.objects.get(id=page_id) @@ -321,7 +321,7 @@ def account(request): return render(request, 'error.html', {'data': data}) # Lastly, do we have to remove an extra conf? - + delete_extra_conf_uuid = request.GET.get('delete_extra_conf_uuid', None) if delete_extra_conf_uuid: #logger.debug('Deleting extra conf "{}"'.format(delete_extra_conf_uuid)) @@ -332,9 +332,9 @@ def account(request): profile.extra_confs = new_extra_confs profile.save() return redirect('/account') - - - + + + return render(request, 'account.html', {'data': data}) @@ -357,13 +357,13 @@ def tasks(request): uuid = request.GET.get('uuid', None) fromlist = request.GET.get('fromlist', False) details = booleanize(request.GET.get('details', None)) - + # Do we have to operate on a specific task? if uuid: try: - + # Get the task (raises if none available including no permission) try: task = Task.objects.get(user=request.user, uuid=uuid) @@ -371,7 +371,7 @@ def tasks(request): raise ErrorMessage('Task does not exists or no access rights') data['task'] = task - + # Task actions if action=='delete': if task.status not in [TaskStatuses.stopped, TaskStatuses.exited]: @@ -392,20 +392,20 @@ def tasks(request): os.remove('/shared/etc_apache2_sites_enabled/{}.htpasswd'.format(task.uuid)) except: pass - + # Delete task.delete() - + # Unset task - data['task'] = None - + data['task'] = None + except Exception as e: data['error'] = 'Error in deleting the task' logger.error('Error in deleting task with uuid="{}": "{}"'.format(uuid, e)) return render(request, 'error.html', {'data': data}) - - + + elif action=='stop': # or delete,a and if delete also remove object # Remove proxy files. Do it here or will cause issues when reloading the conf re-using ports of stopped tasks. try: @@ -416,7 +416,7 @@ def tasks(request): os.remove('/shared/etc_apache2_sites_enabled/{}.htpasswd'.format(task.uuid)) except: pass - + task.computing.manager.stop_task(task) except Exception as e: @@ -441,18 +441,18 @@ def tasks(request): # Get all tasks for list try: - tasks = Task.objects.filter(user=request.user).order_by('created') + tasks = Task.objects.filter(user=request.user).order_by('created') except Exception as e: data['error'] = 'Error in getting Tasks info' logger.error('Error in getting Virtual Devices: "{}"'.format(e)) return render(request, 'error.html', {'data': data}) - + # Update task statuses for task in tasks: task.update_status() - + # Set task and tasks variables - data['task'] = None + data['task'] = None data['tasks'] = tasks return render(request, 'tasks.html', {'data': data}) @@ -473,7 +473,7 @@ def new_task(request): def get_task_container(request): task_container_uuid = request.POST.get('task_container_uuid', None) if not task_container_uuid: - # At the second step the task uuid is set via a GET request + # At the second step the task uuid is set via a GET request task_container_uuid = request.GET.get('task_container_uuid', None) try: task_container = Container.objects.get(uuid=task_container_uuid, user=None) @@ -493,7 +493,7 @@ def new_task(request): try: task_computing = Computing.objects.get(uuid=task_computing_uuid, group__user=request.user) except Computing.DoesNotExist: - raise Exception('Consistency error, computing with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_computing_uuid, request.user.email)) + raise Exception('Consistency error, computing with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_computing_uuid, request.user.email)) return task_computing # Get task name helper function @@ -507,22 +507,22 @@ def new_task(request): step = request.POST.get('step', None) if not step: step = request.GET.get('step', None) - + # Handle the various steps if not step: - + # Step one is assumed: chose software container return HttpResponseRedirect('/software/?mode=new_task') - + elif step == 'two': - + # Get software container and arch data['task_container'] = get_task_container(request) - # List all computing resources + # List all computing resources data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user)) - + data['step'] = 'two' data['next_step'] = 'three' @@ -533,23 +533,23 @@ def new_task(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 engine/engine when creating the task # TODO: refactor and unroll this code if data['task_computing'].supported_archs is None: data['task_computing'].supported_archs=[] if data['task_computing'].emulated_archs is None: data['task_computing'].emulated_archs={} data['arch_emulation'] = False - + 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): # Does container engines/engines support emulated archs? if data['task_computing'].emulated_archs: - + # For now by default our container engine is the first one container_engine = data['task_computing'].container_engines[0] - + # Check for emulation against the engine if container_engine in data['task_computing'].emulated_archs and data['task_container'].image_arch in data['task_computing'].emulated_archs[container_engine]: data['arch_emulation'] = True @@ -561,14 +561,14 @@ def new_task(request): else: container_engines = container_engine.split('[')[1].replace(']','').split(',') return container_engines - + for container_engine in get_engines(container_engine): if container_engine in data['task_computing'].emulated_archs and data['task_container'].image_arch in data['task_computing'].emulated_archs[container_engine]: data['arch_emulation'] = True if not data['arch_emulation']: 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)) @@ -576,7 +576,7 @@ def new_task(request): data['arch_auto_selection'] = True #raise ErrorMessage('Auto selecting architectures is not supported yet') - # Generate random auth token + # Generate random auth token data['task_auth_token'] = str(uuid.uuid4()) # Set current and next step @@ -624,7 +624,7 @@ def new_task(request): # Any task requires the TCP tunnel for now task.requires_tcp_tunnel = True - + # Task access method access_method = request.POST.get('access_method', 'auto') if access_method and access_method != 'auto' and not request.user.profile.is_power_user: @@ -635,10 +635,10 @@ def new_task(request): task.requires_proxy_auth = True else: task.requires_proxy = False - task.requires_proxy_auth = False + task.requires_proxy_auth = False elif access_method == 'direct_tunnel': task.requires_proxy = False - task.requires_proxy_auth = False + task.requires_proxy_auth = False elif access_method == 'https_proxy': task.requires_proxy = True task.requires_proxy_auth = True @@ -647,7 +647,7 @@ def new_task(request): # Computing options computing_options = {} - + # Container engine if any set container_engine = request.POST.get('container_engine', None) if container_engine: @@ -655,11 +655,11 @@ def new_task(request): raise ErrorMessage('Unknown container engine "{}"'.format(container_engine)) computing_options['container_engine'] = container_engine - # CPUs, memory and partition if set + # CPUs, memory and partition if set computing_cpus = request.POST.get('computing_cpus', None) computing_memory = request.POST.get('computing_memory', None) computing_partition = request.POST.get('computing_partition', None) - + if computing_cpus: try: int(computing_cpus) @@ -671,11 +671,11 @@ def new_task(request): computing_options['memory'] = computing_memory if computing_partition: - computing_options['partition'] = computing_partition - + computing_options['partition'] = computing_partition + if computing_options: task.computing_options = computing_options - + # Save the task before starting it, or the computing manager will not be able to work properly task.save() @@ -685,22 +685,22 @@ def new_task(request): except: # Delete the task if could not start it task.delete() - + # ..and re-raise raise # Ensure proxy conf directory exists if not os.path.exists('/shared/etc_apache2_sites_enabled'): os.makedirs('/shared/etc_apache2_sites_enabled') - + # Add here proxy auth file as we have the password if task.requires_proxy_auth: out = os_shell('ssh -o StrictHostKeyChecking=no proxy "cd /shared/etc_apache2_sites_enabled/ && htpasswd -bc {}.htpasswd {} {}"'.format(task.uuid, task.user.email, task.password), capture=True) if out.exit_code != 0: logger.error(out.stderr) - raise ErrorMessage('Something went wrong when enabling proxy auth') + raise ErrorMessage('Something went wrong when enabling proxy auth') - # Set step + # Set step data['step'] = 'created' @@ -731,7 +731,7 @@ def task_log(request): task = Task.objects.get(user=request.user, uuid=uuid) # Set back task and refresh - data['task'] = task + data['task'] = task data['refresh'] = refresh # Get the log @@ -793,7 +793,7 @@ def software(request): try: container = Container.objects.get(uuid=container_uuid) except Container.DoesNotExist: - raise ErrorMessage('Container does not exists or no access rights') + raise ErrorMessage('Container does not exists or no access rights') if container.user and container.user != request.user: raise ErrorMessage('Container does not exists or no access rights') data['container'] = container @@ -803,7 +803,7 @@ def software(request): # Delete container.delete() - + # Redirect return HttpResponseRedirect('/software') @@ -811,20 +811,20 @@ 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}) - + else: # 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: - + + 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)) @@ -833,24 +833,24 @@ def software(request): else: user_containers = Container.objects.filter(user=request.user) platform_containers = Container.objects.filter(user=None) - - + + # Ok, nilter by owner if search_owner != 'All': if search_owner == 'User': platform_containers =[] if search_owner == 'Platform': user_containers = [] - + # Create all container list data['containers'] = list(user_containers) + list(platform_containers) - + # Merge containers with the same name, registry and image 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 @@ -859,34 +859,34 @@ def software(request): self.description = None self.members = [] self.all_archs = [] - self.container_by_tags_by_arch = {} - + 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']: @@ -897,7 +897,7 @@ def software(request): # 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}) @@ -912,7 +912,7 @@ def add_software(request): # Init data data = {} data['user'] = request.user - + # Loop back the new container mode in the page to handle the switch data['new_container_from'] = request.GET.get('new_container_from', 'registry') @@ -920,59 +920,59 @@ def add_software(request): container_name = request.POST.get('container_name', None) if container_name: - + # How do we have to add this new container? new_container_from = request.POST.get('new_container_from', None) if new_container_from == 'registry': - + # Container description container_description = request.POST.get('container_description', None) - + # Container registry container_registry = request.POST.get('container_registry', None) - + # Container image name container_image_name = request.POST.get('container_image_name',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 image OS + + # Container image OS container_image_os = request.POST.get('container_image_os', None) - + # 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) - if container_interface_port: + container_interface_port = request.POST.get('container_interface_port', None) + if container_interface_port: try: container_interface_port = int(container_interface_port) except: raise ErrorMessage('Invalid container port "{}"') else: container_interface_port = None - - # Container interface protocol + + # Container interface protocol container_interface_protocol = request.POST.get('container_interface_protocol', None) - + if container_interface_protocol and not container_interface_protocol in ['http','https']: raise ErrorMessage('Sorry, only power users can add custom software containers with interface protocols other than \'http\' or \'https\'.') - - # Container interface transport + + # Container interface transport container_interface_transport = request.POST.get('container_interface_transport') - + # Capabilities container_supports_custom_interface_port = request.POST.get('container_supports_custom_interface_port', None) if container_supports_custom_interface_port and container_supports_custom_interface_port == 'True': container_supports_custom_interface_port = True else: container_supports_custom_interface_port = False - + container_supports_interface_auth = request.POST.get('container_supports_interface_auth', None) if container_supports_interface_auth and container_supports_interface_auth == 'True': container_supports_interface_auth = True @@ -983,16 +983,16 @@ def add_software(request): if container_disable_http_basicauth_embedding and container_disable_http_basicauth_embedding == 'True': container_disable_http_basicauth_embedding = True else: - container_disable_http_basicauth_embedding = False + container_disable_http_basicauth_embedding = False # Environment variables container_env_vars = request.POST.get('container_env_vars', None) if container_env_vars: container_env_vars = sanitize_container_env_vars(json.loads(container_env_vars)) - + # Log #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, @@ -1010,21 +1010,21 @@ def add_software(request): supports_interface_auth = container_supports_interface_auth, disable_http_basicauth_embedding = container_disable_http_basicauth_embedding, env_vars = container_env_vars) - + elif new_container_from == 'repository': container_description = request.POST.get('container_description', None) - + repository_url = request.POST.get('repository_url', None) repository_tag = request.POST.get('repository_tag', 'HEAD') return HttpResponseRedirect('/import_repository/?repository_url={}&repository_tag={}&container_name={}&container_description={}'.format(repository_url,repository_tag,container_name,container_description)) - + # The return type here is a container, not created #get_or_create_container_from_repository(request.user, repository_url, repository_tag=repository_tag, container_name=container_name, container_description=container_description) else: - raise Exception('Unknown new container mode "{}"'.format(new_container_from)) + raise Exception('Unknown new container mode "{}"'.format(new_container_from)) # Set added switch data['added'] = True @@ -1051,7 +1051,7 @@ def computing(request): computing_uuid = request.GET.get('uuid', None) data['details'] = details data['action'] = action - + if details and computing_uuid: try: data['computing'] = Computing.objects.get(uuid=computing_uuid, group__user=request.user) @@ -1059,7 +1059,7 @@ def computing(request): data['computing'] = Computing.objects.get(uuid=computing_uuid, group=None) else: data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user)) - + return render(request, 'computing.html', {'data': data}) @@ -1072,7 +1072,7 @@ def storage(request): # Set data & render data = {} data['user'] = request.user - + return render(request, 'storage.html', {'data': data}) @@ -1081,17 +1081,17 @@ def storage(request): #========================= # Add profile conf #========================= - + @private_view def add_profile_conf(request): - + # Init data data={} data['user'] = request.user - + # Set conf types we can add data['conf_types'] = ['computing_user'] #,'computing_custom_binds'] - + # Process adding the new conf conf_type = request.POST.get('conf_type', None) if conf_type: @@ -1102,7 +1102,7 @@ def add_profile_conf(request): try: computing = Computing.objects.get(uuid=computing_uuid, group__user=request.user) except Computing.DoesNotExist: - computing = Computing.objects.get(uuid=computing_uuid, group=None) + computing = Computing.objects.get(uuid=computing_uuid, group=None) data['computing'] = computing data['last_step'] = True value = request.POST.get('value', None) @@ -1110,13 +1110,13 @@ def add_profile_conf(request): request.user.profile.add_extra_conf(conf_type=conf_type, object=computing, value=value) # Now redirect to site return HttpResponseRedirect('/account/') - + else: data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user)) else: raise ErrorMessage('Unknown conf type \'{}\''.format(conf_type)) - - + + return render(request, 'add_profile_conf.html', {'data': data}) @@ -1131,15 +1131,15 @@ def task_connect(request): if not task_uuid: raise ErrorMessage('Empty task uuid') - # Get the task + # Get the task task = Task.objects.get(uuid=task_uuid) - + if task.user != request.user: raise ErrorMessage('You do not have access to this task.') # Ensure that the tunnel and proxy are set up setup_tunnel_and_proxy(task) - + # Set default interface status as unknown task.interface_status = 'unknown' @@ -1177,7 +1177,7 @@ def task_connect(request): # task.interface_status = 'running' data ={} - data['task'] = task + data['task'] = task return render(request, 'task_connect.html', {'data': data}) @@ -1188,7 +1188,7 @@ def task_connect(request): @private_view def direct_connection_handler(request, uuid): - # Get the task + # Get the task #task = Task.objects.get(uuid__startswith=short_uuid) task = Task.objects.get(uuid=uuid) @@ -1197,26 +1197,26 @@ def direct_connection_handler(request, uuid): # Ensure that the tunnel and proxy are set up setup_tunnel_and_proxy(task) - + # Get task and tunnel proxy host rosetta_tasks_proxy_host = get_rosetta_tasks_proxy_host() rosetta_tasks_tunnel_host = get_rosetta_tasks_tunnel_host() - # Redirect to the task through the tunnel + # Redirect to the task through the tunnel if task.requires_proxy: if task.requires_proxy_auth and task.auth_token and not task.container.disable_http_basicauth_embedding: user = request.user.email password = task.auth_token - redirect_string = 'https://{}:{}@{}:{}'.format(user, password, rosetta_tasks_proxy_host, task.tcp_tunnel_port) + redirect_string = 'https://{}:{}@{}:{}'.format(user, password, rosetta_tasks_proxy_host, task.tcp_tunnel_port) else: - redirect_string = 'https://{}:{}'.format(rosetta_tasks_proxy_host, task.tcp_tunnel_port) + redirect_string = 'https://{}:{}'.format(rosetta_tasks_proxy_host, task.tcp_tunnel_port) else: redirect_string = '{}://{}:{}'.format(task.container.interface_protocol, rosetta_tasks_tunnel_host, task.tcp_tunnel_port) - + logger.debug('Task direct connect redirect: "{}"'.format(redirect_string)) return redirect(redirect_string) - - + + #=========================== # Sharable link handler @@ -1227,20 +1227,20 @@ def sharable_link_handler(request, short_uuid): # Get the task (if the short uuid is not enough an error wil be raised) task = Task.objects.get(uuid__startswith=short_uuid) - + # First ensure that the tunnel and proxy are set up setup_tunnel_and_proxy(task) - + # Get task and tunnel proxy host rosetta_tasks_proxy_host = get_rosetta_tasks_proxy_host() rosetta_tasks_tunnel_host = get_rosetta_tasks_tunnel_host() - # Redirect to the task through the tunnel + # Redirect to the task through the tunnel if task.requires_proxy: - redirect_string = 'https://{}:{}'.format(rosetta_tasks_proxy_host, task.tcp_tunnel_port) + redirect_string = 'https://{}:{}'.format(rosetta_tasks_proxy_host, task.tcp_tunnel_port) else: redirect_string = '{}://{}:{}'.format(task.container.interface_protocol, rosetta_tasks_tunnel_host, task.tcp_tunnel_port) - + logger.debug('Task sharable link connect redirect: "{}"'.format(redirect_string)) return redirect(redirect_string) @@ -1258,8 +1258,8 @@ def new_binder_task(request, repository): # Convert the Git repository as a Docker container logger.debug('Got a new Binder task request for repository "%s"', repository) - - # Set repository name/tag/url + + # Set repository name/tag/url repository_tag = repository.split('/')[-1] repository_url = repository.replace('/'+repository_tag, '') @@ -1267,13 +1267,13 @@ def new_binder_task(request, repository): # Here i work around this, but TODO: understand what the hell is going on. if 'https:/' in repository_url and not 'https://' in repository_url: repository_url = repository_url.replace('https:/', 'https://') - + if not repository_tag: repository_tag='HEAD' data['repository_url'] = repository_url data['repository_tag'] = repository_tag - + data['mode'] = 'new_task' #new container # Render the import page. This will call an API, and when the import is done, it @@ -1291,17 +1291,17 @@ def import_repository(request): # Init data data={} data['user'] = request.user - + repository_url = request.GET.get('repository_url', None) # I have no idea why the https:// of the repo part of the url gets transfrmed in https:/ # Here i work around this, but TODO: understand what the hell is going on. if 'https:/' in repository_url and not 'https://' in repository_url: repository_url = repository_url.replace('https:/', 'https://') - + repository_tag= request.GET.get('repository_tag', None) if not repository_tag: repository_tag='HEAD' - + data['repository_url'] = repository_url data['repository_tag'] = repository_tag @@ -1314,3 +1314,4 @@ def import_repository(request): # will automatically say "Ok, crrated, go to software". return render(request, 'import_repository.html', {'data': data}) + diff --git a/services/webapp/code/rosetta/settings.py b/services/webapp/code/rosetta/settings.py index 9f854ac..a1ac099 100644 --- a/services/webapp/code/rosetta/settings.py +++ b/services/webapp/code/rosetta/settings.py @@ -43,7 +43,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'rest_framework', 'rest_framework_swagger', - 'django_extensions', + 'django_extensions', ] MIDDLEWARE = [ @@ -176,7 +176,7 @@ ROSETTA_LOG_LEVEL = os.environ.get('ROSETTA_LOG_LEVEL','ERROR') LOGGING = { 'version': 1, 'disable_existing_loggers': False, - + 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(process)d ' @@ -187,13 +187,13 @@ LOGGING = { 'datefmt': '%m/%d/%Y %I:%M:%S %p' } }, - + 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, - + 'handlers': { 'mail_admins': { 'level': 'ERROR', @@ -206,18 +206,18 @@ LOGGING = { 'formatter': 'halfverbose', }, }, - + 'loggers': { 'rosetta': { 'handlers': ['console'], 'level': ROSETTA_LOG_LEVEL, - 'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. + 'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. }, 'django': { 'handlers': ['console'], 'level': DJANGO_LOG_LEVEL, - 'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. - }, + 'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. + }, # Read more about the 'django' logger: https://docs.djangoproject.com/en/2.2/topics/logging/#django-logger # Read more about logging in the right way: https://lincolnloop.com/blog/django-logging-right-way/ } @@ -246,7 +246,7 @@ if OIDC_RP_CLIENT_ID: 'django.contrib.auth.backends.ModelBackend', 'rosetta.auth.RosettaOIDCAuthenticationBackend' ) - + # Base OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET') OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT') @@ -255,7 +255,7 @@ if OIDC_RP_CLIENT_ID: OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'RS256') OIDC_RP_IDP_SIGN_KEY = os.environ.get('OIDC_RP_IDP_SIGN_KEY', None) OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT', None) - + # Check if OIDC_RP_SIGN_ALGO == 'RS256': if not OIDC_RP_IDP_SIGN_KEY and not OIDC_OP_JWKS_ENDPOINT: @@ -267,7 +267,7 @@ if OIDC_RP_CLIENT_ID: # Custom callback to enable session-based post-login redirects OIDC_CALLBACK_CLASS = 'rosetta.auth.RosettaOIDCAuthenticationCallbackView' - + # Non-customizable stuff LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' @@ -277,7 +277,8 @@ if OIDC_RP_CLIENT_ID: # Required for the Open ID connect redirects to work properly USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - + + diff --git a/services/webapp/code/rosetta/urls.py b/services/webapp/code/rosetta/urls.py index 4e0f57f..06c2d0a 100644 --- a/services/webapp/code/rosetta/urls.py +++ b/services/webapp/code/rosetta/urls.py @@ -35,7 +35,7 @@ from rest_framework.documentation import include_docs_urls from rest_framework_swagger.views import get_swagger_view urlpatterns = [ - + # Pages url(r'^$', core_app_views.entrypoint), path('main/', core_app_views.main_view), @@ -44,24 +44,24 @@ urlpatterns = [ url(r'^register/$', core_app_views.register_view), url(r'^pages/(?P<page_id>\w{0,36})/$', core_app_views.page_view), - - # Software + + # Software url(r'^software/$', core_app_views.software), - url(r'^add_software/$', core_app_views.add_software), - url(r'^import_repository/$', core_app_views.import_repository), - + url(r'^add_software/$', core_app_views.add_software), + url(r'^import_repository/$', core_app_views.import_repository), + #Computing url(r'^computing/$', core_app_views.computing), - + # Storage url(r'^storage/$', core_app_views.storage), - + # Tasks url(r'^tasks/$', core_app_views.tasks), url(r'^new_task/$', core_app_views.new_task), url(r'^task_log/$', core_app_views.task_log), url(r'^task_connect/$', core_app_views.task_connect), - + # Sharable and direct connection links for tasks url(r'^direct_connect/(?P<uuid>[0-9a-f-]+)/$', core_app_views.direct_connection_handler), url(r'^t/(?P<short_uuid>\w{0,36})/$', core_app_views.sharable_link_handler), @@ -76,7 +76,7 @@ urlpatterns = [ # Admin and API docs (Swagger) path('admin/', admin.site.urls), path('api/v1/doc/', get_swagger_view(title="Swagger Documentation")), - + # APIs path('api/v1/base/login/', core_app_api.login_api.as_view(), name='login_api'), path('api/v1/base/logout/', core_app_api.logout_api.as_view(), name='logout_api'), @@ -97,7 +97,7 @@ urlpatterns = [ # Get admin files location admin_files_path = '/'.join(django.__file__.split('/')[0:-1]) + '/contrib/admin/static/admin' - + if not settings.DEBUG: # Admin files @@ -105,7 +105,7 @@ if not settings.DEBUG: # Rosetta Core app files document_root = 'rosetta/core_app/static' - + if os.path.isdir(document_root): logger.info('Serving static files for app "core_app" from document root "{}"'.format(document_root)) # Static @@ -116,3 +116,4 @@ else: logger.info('Not serving static files at all as DEBUG=True (Django will do it automatically)') + diff --git a/services/webapp/prestartup_webapp.sh b/services/webapp/prestartup_webapp.sh index e986a46..6688bba 100644 --- a/services/webapp/prestartup_webapp.sh +++ b/services/webapp/prestartup_webapp.sh @@ -9,5 +9,3 @@ mkdir -p /data/resources chown rosetta:rosetta /data/resources chown rosetta:rosetta /shared - - diff --git a/services/webapp/run_webapp.sh b/services/webapp/run_webapp.sh index 622b6f4..5e0070c 100644 --- a/services/webapp/run_webapp.sh +++ b/services/webapp/run_webapp.sh @@ -35,7 +35,7 @@ echo "" if [[ "x$DJANGO_DEV_SERVER" == "xTrue" ]] ; then - + # Run the (development) server echo "Now starting the development server and logging in /var/log/webapp/server.log." exec python3 manage.py runserver 0.0.0.0:8080 2>> /var/log/webapp/server.log @@ -43,7 +43,7 @@ if [[ "x$DJANGO_DEV_SERVER" == "xTrue" ]] ; then else # Move to the code dir cd /opt/code - + # Collect static echo "Collecting static files..." python3 manage.py collectstatic @@ -63,20 +63,3 @@ else --disable-logging 2>> /var/log/webapp/server.log fi - - - - - - - - - - - - - - - - - -- GitLab