diff --git a/.gitignore b/.gitignore index 6b2411b4de447d50510d630dcd7e92860816f883..0973438037150f95f1e9c26b726a05eb391fa546 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ __pycache__/ # Data data* +# DB conf +images/webapp/db_conf.sh diff --git a/README.md b/README.md index c27286cc496cc27b6992174dbc5558a70b3fd1dc..5eb036bd670c671d69ded1c6cb4c942c6481dc75 100755 --- a/README.md +++ b/README.md @@ -27,13 +27,53 @@ Run $ rosetta/run -Check status - $ rosetta/ps +Play + + You can now point your browser to http://localhost:8080 + +Clean + + # rosetta/clean + +### Extras + +Check status (not yet fully supported) + + # rosetta/status + + +Run Web App unit tests (with Rosetta running) + + ./run_webapp_unit_tests.sh ### Building errors It is common for the build process to fail with a "404 not found" error on an apt-get instrucions, as apt repositories often change their IP addresses. In such case, try: - $ rosetta/build nocache \ No newline at end of file + $ rosetta/build nocache + + +### Development mode + +Django development server is running on port 8080 of the "webapp" service. + +To enable live code changes, add or comment out the following in docker-compose.yaml under the "volumes" section of the "webapp" service: + + - ./images/webapp/code:/opt/webapp_code + +This will mount the code from images/webapp/code as a volume inside the webapp container itself allowing to make immediately effective codebase edits. + +Note that when you edit the Django ORM model, you need to rerun the migrate the database, either by just rerunning the webapp service: + + $ rosetta/rerun webapp + +..ora by entering in the webapp service container and manually migrate: + + $ rosetta/shell webapp + $ source /env.sh + $ source /db_conf.sh + $ cd /opt/webapp_code + $ python3 manage.py makemigrations + $ python3 manage.py migrate \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 474fb751ac908b6fc612bb03928b0651e47233ac..b8b5435840b5d90936bf9750f3fb28a5ff6b57cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,3 +33,26 @@ services: - ./data/dregistry:/var/lib/registry ports: - "5000:5000" + + webapp: + image: "rosetta/webapp" + container_name: webapp + hostname: webapp + environment: + - SAFEMODE=False + - DJANGO_LOG_LEVEL=CRITICAL + - ROSETTA_LOG_LEVEL=DEBUG + ports: + - "8080:8080" + volumes: + - ./data_rosetta/webapp/data:/data + - ./data_rosetta/webapp/log:/var/log/webapp + #- ./images/webapp/code:/opt/webapp_code + + + + + + + + diff --git a/images/webapp/.gitignore b/images/webapp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d007c6ac92e4d87d7e0d456d52572eac7d5b0b2c --- /dev/null +++ b/images/webapp/.gitignore @@ -0,0 +1,129 @@ +# From https://github.com/github/gitignore/blob/master/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +virtual/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# +.docker_bash_history diff --git a/images/webapp/Dockerfile b/images/webapp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b8cb025cc103805b7cca8de6fead32b6ab662795 --- /dev/null +++ b/images/webapp/Dockerfile @@ -0,0 +1,90 @@ +FROM rosetta/base +MAINTAINER Stefano Alberto Russo <stefano.russo@gmail.com> + +# Always start with an apt-get update when extending base images, +# otherwise apt repositories might get outdated (404 not found) +# and building without cache does not re-build base images. +RUN apt-get update + +#------------------------------ +# Apt requirements +#------------------------------ + +# Install Curl +RUN apt-get install curl -y + +# Download get-pip script +RUN curl -O https://bootstrap.pypa.io/get-pip.py + +# Install Python3 and Pip3 (python3-distutils required for pip3) +RUN apt-get install python3 python3-distutils -y + +# Install Python and pip in this order (first Python 3 and then Python 2), or +# you will end ap with python defaulting to python2 and pip defaulting to pip3 +# Otherwise, do somethign like "ln -s /usr/local/bin/pip3 /usr/local/bin/pip" + +# Install Python3 and Pip3 (ython3-distutils required for pip3) +RUN apt-get install python3 python3-distutils -y +RUN python3 get-pip.py 'pip==10.0.1' + +# Install Python2 and Pip2 +RUN apt-get install python -y +RUN python get-pip.py 'pip==10.0.1' + +# Python 3 dev (for pycrypto) +RUN apt-get install python3-dev -y + +# Install postgres driver required for psycopg2 +RUN apt-get install libpq-dev -y + + +#------------------------------ +# Install Django project +#------------------------------ + +# Prepare dir +RUN mkdir /opt/webapp_code + +# Install Python requirements.. +COPY requirements.txt /tmp/ +RUN cd /opt/webapp_code && pip3 install -r /tmp/requirements.txt + +# Patch Django 2.2 non-ascii chars in /usr/local/lib/python3.6/dist-packages/django/views/templates/technical_500.html +RUN sed -i 's/[\x80-\xFF]/./g' /usr/local/lib/python3.6/dist-packages/django/views/templates/technical_500.html + +# Install App code +COPY code /opt/webapp_code + +# Fix permissions +RUN chown -R rosetta:rosetta /opt/webapp_code + +# Copy db conf +COPY db_conf.sh /db_conf.sh + +# Prepare for logs +RUN mkdir /var/log/webapp/ && chown rosetta:rosetta /var/log/webapp/ + + +#------------------------------ +# Supervisord +#------------------------------ + +COPY run_webapp.sh /etc/supervisor/conf.d/ +RUN chmod 755 /etc/supervisor/conf.d/run_webapp.sh +COPY supervisord_webapp.conf /etc/supervisor/conf.d/ + + +#------------------------------ +# Prestartup +#------------------------------ + +COPY prestartup_webapp.sh /prestartup/ + + + + + + + + + diff --git a/images/webapp/code/manage.py b/images/webapp/code/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..ddb292f3784b73c511921d727b8665ba6277b522 --- /dev/null +++ b/images/webapp/code/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rosetta.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/images/webapp/code/migrations_vanilla/README.txt b/images/webapp/code/migrations_vanilla/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd641869d4ab930e2c8585f5a412fa96d618a5bf --- /dev/null +++ b/images/webapp/code/migrations_vanilla/README.txt @@ -0,0 +1,3 @@ + +This is a folder with just an __init__.py file, used to initialize the migrations. + diff --git a/images/webapp/code/migrations_vanilla/__init__.py b/images/webapp/code/migrations_vanilla/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/images/webapp/code/rosetta/__init__.py b/images/webapp/code/rosetta/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/images/webapp/code/rosetta/base_app/__init__.py b/images/webapp/code/rosetta/base_app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/images/webapp/code/rosetta/base_app/api.py b/images/webapp/code/rosetta/base_app/api.py new file mode 100644 index 0000000000000000000000000000000000000000..c2b785867aa45a1a6b7ec6269cf7bfbdfc566568 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/api.py @@ -0,0 +1,220 @@ +import logging + +# Django imports +from django.http import HttpResponse +from django.utils import timezone +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.models import User, Group + +from rest_framework.response import Response +from rest_framework import status, serializers, viewsets +from rest_framework.views import APIView + + +# Project imports +from rosetta.common import format_exception +from rosetta.base_app.models import Profile + +# Setup logging +logger = logging.getLogger(__name__) + + +#============================== +# 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 + user = request.user if request.user.is_authenticated else None + username = request.data.get('username', None) + password = request.data.get('password', None) + authtoken = request.data.get('authtoken', None) + + # Try standard user authentication + if user: + return user + + # 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') + else: + login(request, user) + return user + + # Try auth toekn authentication + elif authtoken: + try: + profile = Profile.objects.get(authtoken=authtoken) + except Profile.DoesNotExist: + return error400('Wrong auth token') + login(request, profile.user) + return profile.user + else: + return error401('This is a private API. Login or provide username/password or auth token') + + + +#============================== +# Base public API class +#============================== + +class PublicPOSTAPI(APIView): + '''Base public POST API class''' + + # POST + def post(self, request): + try: + return self._post(request) + 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''' + # GET + def get(self, request): + try: + return self._get(request) + except Exception as e: + logger.error(format_exception(e)) + return error500('Got error in processing request: {}'.format(e)) + + + +#============================== +# 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 + 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: + logger.error(format_exception(e)) + return error500('Got error in processing request: {}'.format(e)) + + + +#============================== +# User & profile APIs +#============================== + +class login_api(PrivateGETAPI, PrivatePOSTAPI): + """ + get: + Returns the auth token. + + 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}) + + +class logout_api(PrivateGETAPI): + """ + get: + Logout the user + """ + + def _get(self, request): + logout(request) + return ok200() + + +class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows Users to be viewed or edited. + """ + + class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ('url', 'username', 'email', 'groups') + + queryset = User.objects.all().order_by('-date_joined') + serializer_class = UserSerializer + + + + + + + + + diff --git a/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py new file mode 100644 index 0000000000000000000000000000000000000000..02666637130396d35347bc9c9da8f38b92144841 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py @@ -0,0 +1,25 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +from ...models import Profile + +class Command(BaseCommand): + help = 'Adds the admin superuser with \'a\' password.' + + def handle(self, *args, **options): + + try: + User.objects.get(username='admin') + print('Not creating admin user as it already exist') + except User.DoesNotExist: + print('Creating admin user with default password') + admin = User.objects.create_superuser('admin', 'admin@example.com', 'a') + Profile.objects.create(user=admin) + + try: + User.objects.get(username='testuser') + print('Not creating test user as it already exist') + except User.DoesNotExist: + print('Creating test user with default password') + testuser = User.objects.create_user('testuser', 'testuser@rosetta.platform', 'testpass') + Profile.objects.create(user=testuser, authtoken='129aac94-284a-4476-953c-ffa4349b4a50') + \ No newline at end of file diff --git a/images/webapp/code/rosetta/base_app/migrations b/images/webapp/code/rosetta/base_app/migrations new file mode 120000 index 0000000000000000000000000000000000000000..34d39117ff8ff780d1513412adb3dd1d68df3d65 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/migrations @@ -0,0 +1 @@ +/data/migrations/base_app \ No newline at end of file diff --git a/images/webapp/code/rosetta/base_app/models.py b/images/webapp/code/rosetta/base_app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..4cb132d3336faf5a7d76a9a36758f83d340b1114 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/models.py @@ -0,0 +1,31 @@ +import uuid +from django.db import models +from django.contrib.auth.models import User + + +#========================= +# Profile +#========================= + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + timezone = models.CharField('User Timezone', max_length=36, default='UTC') + authtoken = models.CharField('User auth token', max_length=36, blank=True, null=True) + + def save(self, *args, **kwargs): + if not self.authtoken: + self.authtoken = str(uuid.uuid4()) + super(Profile, self).save(*args, **kwargs) + + def __unicode__(self): + return str('Profile of user "{}"'.format(self.user.username)) + + + + + + + + + + diff --git a/images/webapp/code/rosetta/base_app/templates/login.html b/images/webapp/code/rosetta/base_app/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..ec1d91f66babd6c46a26053ab6c8521200d472e8 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/templates/login.html @@ -0,0 +1,30 @@ +<html> + <head> + <title>Rosetta WebApp</title> + </head> + <body> + <div style="text-align:center; margin-top:50px"> + {% if data.error %} + <div style="color:red"> + {{ data.error }} + </div> + {% elif data.success %} + <div style="color:green"> + {{data.success}} + </div> + {% else %} + <br/> + {% endif %} + + <form class="form-signin" role="form" action='/login/' method='POST'> + {% csrf_token %} + <input style="max-width:300px; margin: 5px auto" type="username" class="form-control" placeholder="Username" name='username' required autofocus> + <input style="max-width:300px; margin: 5px auto" type="password" class="form-control" placeholder="Password" name='password'> + <input style="width:100px; height:35px; margin: 5px auto; font-size:16px" type='submit' class="btn btn-success" value='Log in' /> + </form> + </div> + </body> + + +</html> + diff --git a/images/webapp/code/rosetta/base_app/templates/main.html b/images/webapp/code/rosetta/base_app/templates/main.html new file mode 100644 index 0000000000000000000000000000000000000000..e679084dabfad41d0874a128a0ca6dbce7214e8b --- /dev/null +++ b/images/webapp/code/rosetta/base_app/templates/main.html @@ -0,0 +1,51 @@ +<html> + <head> + <title>Rosetta WebApp</title> + </head> + <body> + <div style="text-align:center; margin-top:50px"> + <h1>Rosetta WebApp</h1> + {% if data.error %} + <div style="color:red"> + {{ data.error }} + </div> + {% elif data.success %} + <div style="color:green"> + {{data.success}} + </div> + {% else %} + <br/> + {% endif %} + + {% if request.user.is_authenticated %} + Logged in as <b>{{ request.user.username }}</b> | <a href="/logout">Logout</a> + {% else %} + <form class="form-signin" role="form" action='/login/' method='POST'> + {% csrf_token %} + <input style="max-width:300px; margin: 5px auto" type="username" class="form-control" placeholder="Username" name='username' required autofocus> + <input style="max-width:300px; margin: 5px auto" type="password" class="form-control" placeholder="Password" name='password'> + <input style="width:100px; height:35px; margin: 5px auto; font-size:16px" type='submit' class="btn btn-success" value='Log in' /> + </form> + {% endif %} + </div> + + <div style="max-width:500px; margin: 30px auto"> + Modules: + <ul style="margin-top:5px; margin-bottom:20px"> + <li><a href="/admin">Admin</a></li> + <li><a href="/api/v1/doc">Swagger APIs v1 Doc</a></li> + </ul> + + REST APIs v1: + <ul style="margin-top:5px"> + <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/images/webapp/code/rosetta/base_app/tests/__init__.py b/images/webapp/code/rosetta/base_app/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/images/webapp/code/rosetta/base_app/tests/common.py b/images/webapp/code/rosetta/base_app/tests/common.py new file mode 100644 index 0000000000000000000000000000000000000000..75a1a7ee302c96dd1a3b1d379834a0383bdcf0d3 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/tests/common.py @@ -0,0 +1,93 @@ +import json +from django.test import TestCase +from rest_framework.test import APIClient as Client +from django.test.client import MULTIPART_CONTENT +from rest_framework import status +from rest_framework.reverse import reverse +from django.contrib.auth.models import User + +class APIClient(Client): + # Add patch to test client object + def patch(self, path, data='', content_type=MULTIPART_CONTENT, follow=False, **extra): + return self.generic('PATCH', path, data, content_type, **extra) + + # Add options to test client object + def options(self, path, data='', content_type=MULTIPART_CONTENT, follow=False, **extra): + return self.generic('OPTIONS', path, data, content_type, **extra) + + +class BaseAPITestCase(TestCase): + + def __init__(self, *args, **kwargs): + self.maxDiff = None + super(TestCase, self).__init__(*args, **kwargs) + + def send_request(self, request_method, *args, **kwargs): + + 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 + 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) + + if 'status_code' in kwargs: + status_code = kwargs.pop('status_code') + + self.response = request_func(*args, **kwargs) + + # Parse response + is_json = False + + if 'content-type' in self.response._headers: + is_json = bool(filter(lambda x: 'json' in x, self.response._headers['content-type'])) + + try: + if is_json and self.response.content: + 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)) + return self.response + + def post(self, *args, **kwargs): + return self.send_request('post', *args, **kwargs) + + def get(self, *args, **kwargs): + return self.send_request('get', *args, **kwargs) + + def put(self, *args, **kwargs): + return self.send_request('put', *args, **kwargs) + + def delete(self, *args, **kwargs): + return self.send_request('delete', *args, **kwargs) + + def patch(self, *args, **kwargs): + return self.send_request('patch', *args, **kwargs) + + def init(self): + self.client = APIClient() + + def assertRedirects(self, *args, **kwargs): + super(BaseAPITestCase, self).assertRedirects(*args, **kwargs) + + + + + + + + diff --git a/images/webapp/code/rosetta/base_app/tests/test_apis.py b/images/webapp/code/rosetta/base_app/tests/test_apis.py new file mode 100644 index 0000000000000000000000000000000000000000..7ef1ed7aedb7ee3dc830e4aa3e326710dbf77832 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/tests/test_apis.py @@ -0,0 +1,49 @@ +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') + + # Create test profile + Profile.objects.create(user=self.user, authtoken='ync719tce917tec197t29cn712eg') + + + def test_api_web_auth(self): + '''Test auth using login api''' + + # No user at all + resp = self.post('/api/v1/base/login/', data={}) + self.assertEqual(resp.status_code, 401) + self.assertEqual(json.loads(resp.content), {"detail": "This is a private API. Login or provide username/password or auth token"}) + + # Wrong user + resp = self.post('/api/v1/base/login/', data={'username':'wronguser', 'password':'testpass'}) + self.assertEqual(resp.status_code, 401) + self.assertEqual(json.loads(resp.content), {"detail": "Wrong username/password"}) + + # Wrong pass + resp = self.post('/api/v1/base/login/', data={'username':'testuser', 'password':'wrongpass'}) + self.assertEqual(resp.status_code, 401) + self.assertEqual(json.loads(resp.content), {"detail": "Wrong username/password"}) + + # Correct user + resp = self.post('/api/v1/base/login/', data={'username': 'testuser', 'password':'testpass'}) + self.assertEqual(resp.status_code, 200) + + + + + + + + + \ No newline at end of file diff --git a/images/webapp/code/rosetta/base_app/views.py b/images/webapp/code/rosetta/base_app/views.py new file mode 100644 index 0000000000000000000000000000000000000000..3b8ec33504a96dbccc248f087cbe31bc5f460c94 --- /dev/null +++ b/images/webapp/code/rosetta/base_app/views.py @@ -0,0 +1,55 @@ +from django.shortcuts import render +from django.http import HttpResponseRedirect +from django.contrib.auth import authenticate, login, logout + +# Setup logging +import logging +logger = logging.getLogger(__name__) + +class ErrorMessage(Exception): + pass + + +def main_view(request): + return render(request, 'main.html') + + +def login_view(request): + + data={} + + if request.method == 'POST': + username = request.POST.get('username') + password = request.POST.get('password') + + if (not username) or (not password): + data['error'] = 'Empty username or password' + + if request.user.is_authenticated: + logout(request) + + user = authenticate(username=username, password=password) + if user: + login(request, user) + return HttpResponseRedirect('/') + else: + data['error'] = 'Check username and password' + + + # Render the login page again with no other data than title + return render(request, 'login.html', {'data': data}) + + + +def logout_view(request): + logout(request) + return HttpResponseRedirect('/') + + + + + + + + + diff --git a/images/webapp/code/rosetta/common.py b/images/webapp/code/rosetta/common.py new file mode 100644 index 0000000000000000000000000000000000000000..418fd24eef319bf745c857b31c1701db79d05246 --- /dev/null +++ b/images/webapp/code/rosetta/common.py @@ -0,0 +1,196 @@ +import pytz +import time +import calendar +import logging +from datetime import datetime +import traceback +from rest_framework import serializers + +try: + from dateutil.tz import tzoffset +except ImportError: + tzoffset = None + +class ConsistencyException(Exception): + pass + +class AlreadyExistentException(Exception): + pass + +class DoNotCommitTransactionException(Exception): + pass + +def format_exception(e): + return 'Exception: ' + str(e) + '; Traceback: ' + traceback.format_exc().replace('\n','|') + +class HyperlinkedModelSerializerWithId(serializers.HyperlinkedModelSerializer): + """Extend the HyperlinkedModelSerializer to add IDs as well for the best of + both worlds. + """ + id = serializers.ReadOnlyField() + + +# def setup_logger(logger, loglevel): +# handler = logging.StreamHandler() +# formatter = logging.Formatter('%(name)s - %(levelname)s: %(message)s') +# handler.setFormatter(formatter) +# logger.addHandler(handler) +# logger.setLevel(loglevel) +# return logger + + +#=================================== +# Time management +#=================================== + +# Note: most of the following routines are extrapolated from the +# time package of the Luna project (https://github.com/sarusso/Luna) +# by courtesy of Stefano Alberto Russo. If you find and fix any bug, +# please open a pull request with the fix for Luna as well. Thank you! + +def timezonize(timezone): + if not 'pytz' in str(type(timezone)): + timezone = pytz.timezone(timezone) + return timezone + +def t_now(): + return time.time() + + +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) + 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: + raise Exception('For ISO date with offset please install dateutil') + time_dt = datetime(*args, tzinfo=tzoffset(None, offset_s)) + else: + # Standard timezone + time_dt = timezone.localize(datetime(*args)) + + # 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)) + + return time_dt + +def dt_from_s(timestamp_s, tz=None): + if not tz: + tz = "UTC" + try: + timestamp_dt = datetime.utcfromtimestamp(float(timestamp_s)) + except TypeError: + raise Exception('timestamp_s argument must be string or number, got {}'.format(type(timestamp_s))) + + pytz_tz = timezonize(tz) + timestamp_dt = timestamp_dt.replace(tzinfo=pytz.utc).astimezone(pytz_tz) + + return timestamp_dt + +def s_from_dt(dt): + if not (isinstance(dt, datetime)): + raise Exception('s_from_dt function called without datetime argument, got type "{}" instead.'.format(dt.__class__.__name__)) + microseconds_part = (dt.microsecond/1000000.0) if dt.microsecond else 0 + return ( calendar.timegm(dt.utctimetuple()) + microseconds_part) + +def check_dt_consistency(date_dt): + if date_dt.tzinfo is None: + return True + else: + if date_dt.utcoffset() != dt_from_s(s_from_dt(date_dt), tz=date_dt.tzinfo).utcoffset(): + return False + else: + return True + +def dt_from_str(string, timezone=None): + + # Supported formats on UTC + # 1) YYYY-MM-DDThh:mm:ssZ + # 2) YYYY-MM-DDThh:mm:ss.{u}Z + + # Supported formats with offset + # 3) YYYY-MM-DDThh:mm:ss+ZZ:ZZ + # 4) YYYY-MM-DDThh:mm:ss.{u}+ZZ:ZZ + + # Also: + # 5) YYYY-MM-DDThh:mm:ss (without the trailing Z, and assume it on UTC) + + # 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 + if ':' in offset: + offset_s = (int(offset[0:2])*60 + int(offset[3:5]))* 60 + else: + offset_s = (int(offset[0:2])*60 + int(offset[2:4]))* 60 + + + elif ('-') in time: + # Negative offset + time, offset = time.split('-') + # Set time and extract negative offset + if ':' in offset: + offset_s = -1 * (int(offset[0:2])*60 + int(offset[3:5]))* 60 + else: + offset_s = -1 * (int(offset[0:2])*60 + int(offset[2:4]))* 60 + + + else: + # Assume UTC + offset_s = 0 + #raise InputException('Format error') + + # Handle time + hour, minute, second = time.split(':') + + # Now parse date (easy) + year, month, day = date.split('-') + + # Convert everything to int + year = int(year) + month = int(month) + day = int(day) + hour = int(hour) + minute = int(minute) + if '.' in second: + usecond = int(second.split('.')[1]) + second = int(second.split('.')[0]) + else: + second = int(second) + usecond = 0 + + return dt(year, month, day, hour, minute, second, usecond, offset_s=offset_s) + + +def dt_to_str(dt): + '''Return the ISO representation of the datetime as argument''' + return dt.isoformat() diff --git a/images/webapp/code/rosetta/settings.py b/images/webapp/code/rosetta/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..58521f810bcc40d8ed391282e3f997d203bff6a5 --- /dev/null +++ b/images/webapp/code/rosetta/settings.py @@ -0,0 +1,211 @@ +""" +Django settings for rosetta project. + +Generated by 'django-admin startproject' using Django 2.2. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '-3byo^nd6-x82fuj*#68mj=5#qp*gagg58sc($u$r-=g8ujxu4' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'rosetta.base_app', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_swagger', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'rosetta.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'rosetta.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +default_db_engine = 'django.db.backends.sqlite3' +default_db_name = os.path.join(BASE_DIR, '../rosetta_database.sqlite3') + +DATABASES = { + 'default': { + 'ENGINE': os.environ.get('DJANGO_DB_ENGINE', default_db_engine), + 'NAME': os.environ.get('DJANGO_DB_NAME', default_db_name), + 'USER': os.environ.get('DJANGO_DB_USER', None), + 'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', None), + 'HOST': os.environ.get('DJANGO_DB_HOST', None), + 'PORT': os.environ.get('DJANGO_DB_PORT',None), + } +} + + + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' + + +# REST framework settings +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100000 +} + +# Swagger settings +# See https://django-rest-swagger.readthedocs.io/en/latest/settings/ + +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITIONS': {}, + 'USE_SESSION_AUTH': False +} + +# Data path for resources etc. +DATA_PATH = '/data/' +TMP_PATH = '/tmp/' + +#=============================== +# Logging +#=============================== + +DJANGO_LOG_LEVEL = os.environ.get('DJANGO_LOG_LEVEL','ERROR') +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 ' + '%(thread)d %(message)s', + }, + 'halfverbose': { + 'format': '%(asctime)s, %(name)s: [%(levelname)s] - %(message)s', + 'datefmt': '%m/%d/%Y %I:%M:%S %p' + } + }, + + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + '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. + }, + '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. + }, + # 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/ + } +} + + + diff --git a/images/webapp/code/rosetta/urls.py b/images/webapp/code/rosetta/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..098d5998859a081c3d5df011023526f510fce379 --- /dev/null +++ b/images/webapp/code/rosetta/urls.py @@ -0,0 +1,54 @@ +"""rosetta URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path +from django.conf.urls import url +import logging + +logger = logging.getLogger(__name__) + +# Base App +from rosetta.base_app import api as base_app_api +from rosetta.base_app import views as base_app_views + +# REST Framework & Swagger +from rest_framework import routers +from rest_framework.documentation import include_docs_urls +from rest_framework_swagger.views import get_swagger_view + +base_app_api_router = routers.DefaultRouter() +base_app_api_router.register(r'users', base_app_api.UserViewSet) + +urlpatterns = [ + + # Webpages + path('', base_app_views.main_view), + path('login/', base_app_views.login_view), + path('logout/', base_app_views.logout_view), + + # Modules + path('admin/', admin.site.urls), + path('api/v1/doc/', get_swagger_view(title="Swagger Documentation")), + + # ViewSet APIs + path('api/v1/base/login/', base_app_api.login_api.as_view(), name='login_api'), + path('api/v1/base/logout/', base_app_api.logout_api.as_view(), name='logout_api'), + +] + +# This message here is quite useful when developing in autoreload mode +logger.info('Loaded URLs') + diff --git a/images/webapp/code/rosetta/wsgi.py b/images/webapp/code/rosetta/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..6c8d40e667ac975d6bdf4f7c9d9a7d4db67130af --- /dev/null +++ b/images/webapp/code/rosetta/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for rosetta project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rosetta.settings') + +application = get_wsgi_application() diff --git a/images/webapp/db_conf-dev.sh b/images/webapp/db_conf-dev.sh new file mode 100644 index 0000000000000000000000000000000000000000..242ede53b4bc3a831241955e9d7491996cc86338 --- /dev/null +++ b/images/webapp/db_conf-dev.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# SQlite Django DB conf +export DJANGO_DB_ENGINE="django.db.backends.sqlite3" +export DJANGO_DB_NAME="/data/database.sqlite3" + +# Postgres Django DB conf +#export DJANGO_DB_ENGINE="django.db.backends.postgresql_psycopg2" +#export DJANGO_DB_NAME="rosetta" +#export DJANGO_DB_USER="rosetta" +#export DJANGO_DB_PASSWORD="" +#export DJANGO_DB_HOST="postgres" +#export DJANGO_DB_PORT=5432 diff --git a/images/webapp/prestartup_webapp.sh b/images/webapp/prestartup_webapp.sh new file mode 100644 index 0000000000000000000000000000000000000000..befdd9ac85231f6e79973421843ea6c12774b324 --- /dev/null +++ b/images/webapp/prestartup_webapp.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e + +# Set proper permissions to the log dir +chown rosetta:rosetta /var/log/webapp + +# Create and set proper permissions to the data/resources +mkdir -p /data/resources +chown rosetta:rosetta /data/resources + +#----------------------------- +# Set migrations data folder +#----------------------------- + +if [[ "x$(mount | grep /devmigrations)" == "x" ]] ; then + # If the migrations folder is not mounted (not a Docker volume), use the /data directory via links to use data persistency + MIGRATIONS_DATA_FOLDER="/data/migrations" + # Also if the migrations folder in /data does not exist, create it now + mkdir -p /data/migrations +else + # If the migrations folder is mounted (a Docker volume), use it as we are in dev mode + MIGRATIONS_DATA_FOLDER="/devmigrations" +fi +echo "Persisting migrations in $MIGRATIONS_DATA_FOLDER" + + +#----------------------------- +# Handle Base App migrations +#----------------------------- + +# Remove potential leftovers +rm -f /opt/webapp_code/rosetta/base_app/migrations +if [ ! -d "$MIGRATIONS_DATA_FOLDER/base_app" ] ; then + # If migrations were not already initialized, do it now + echo "Initializing migrations for base_app"... + mkdir $MIGRATIONS_DATA_FOLDER/base_app && chown rosetta:rosetta $MIGRATIONS_DATA_FOLDER/base_app + touch $MIGRATIONS_DATA_FOLDER/base_app/__init__.py && chown rosetta:rosetta $MIGRATIONS_DATA_FOLDER/base_app/__init__.py +fi + +# Use the persisted migrations +ln -s $MIGRATIONS_DATA_FOLDER/base_app /opt/webapp_code/rosetta/base_app/migrations + + + + + + + + diff --git a/images/webapp/requirements.txt b/images/webapp/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3b7edee7b390437c05b57864ff26db11d8372a01 --- /dev/null +++ b/images/webapp/requirements.txt @@ -0,0 +1,6 @@ +Django==2.2.1 +psycopg2==2.8 +pytz==2018.9 +djangorestframework==3.9.3 +django-rest-swagger==2.2.0 +dateutils==0.6.6 diff --git a/images/webapp/run_webapp.sh b/images/webapp/run_webapp.sh new file mode 100644 index 0000000000000000000000000000000000000000..2e5677800874c489163a5b1bef71ac6ba354e4a0 --- /dev/null +++ b/images/webapp/run_webapp.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +DATE=$(date) + +echo "" +echo "===================================================" +echo " Starting Backend @ $DATE" +echo "===================================================" +echo "" + +echo "1) Loading/sourcing env and settings" + +# Load env +source /env.sh + +# Database conf +source /db_conf.sh + +# Django Project conf +if [[ "x$DJANGO_PROJECT_NAME" == "x" ]] ; then + export DJANGO_PROJECT_NAME="Rosetta" +fi + +if [[ "x$DJANGO_PUBLIC_HTTP_HOST" == "x" ]] ; then + export DJANGO_PUBLIC_HTTP_HOST="https://rosetta.platform" +fi + +if [[ "x$DJANGO_EMAIL_SERVICE" == "x" ]] ; then + export DJANGO_EMAIL_SERVICE="Sendgrid" +fi + +if [[ "x$DJANGO_EMAIL_FROM" == "x" ]] ; then + export DJANGO_EMAIL_FROM="Rosetta <rosetta@rosetta.platform>" +fi + +if [[ "x$DJANGO_EMAIL_APIKEY" == "x" ]] ; then + export DJANGO_EMAIL_APIKEY="" +fi + +# Set log levels +export DJANGO_LOG_LEVEL="CRITICAL" +export ROSETTA_LOG_LEVEL="CRITICAL" + +# Stay quiet on Python warnings +export PYTHONWARNINGS=ignore + +# To Python3 (unbuffered). P.s. "python3 -u" does not work.. +export DJANGO_PYTHON=python3 +export PYTHONUNBUFFERED=on + +# Check if there is something to migrate or populate +echo "" +echo "2) Making migrations..." +cd /opt/webapp_code && $DJANGO_PYTHON manage.py makemigrations --noinput +EXIT_CODE=$? +echo "Exit code: $EXIT_CODE" +if [[ "x$EXIT_CODE" != "x0" ]] ; then + echo "This exit code is an error, sleeping 5s and exiting." + sleep 5 + exit $? +fi +echo "" + +echo "3) Migrating..." +cd /opt/webapp_code && $DJANGO_PYTHON manage.py migrate --noinput +EXIT_CODE=$? +echo "Exit code: $EXIT_CODE" +if [[ "x$EXIT_CODE" != "x0" ]] ; then + echo "This exit code is an error, sleeping 5s and exiting." + sleep 5 + exit $? +fi +echo "" + +echo "4) Populating base app..." +cd /opt/webapp_code && $DJANGO_PYTHON manage.py base_app_populate +EXIT_CODE=$? +echo "Exit code: $EXIT_CODE" +if [[ "x$EXIT_CODE" != "x0" ]] ; then + echo "This exit code is an error, sleeping 5s and exiting." + sleep 5 + exit $? +fi +echo "" + +# Run the (development) server +echo "6) Now starting the server and logging in /var/log/cloud_server.log." +exec $DJANGO_PYTHON manage.py runserver 0.0.0.0:8080 2>> /var/log/webapp/server.log diff --git a/images/webapp/supervisord_webapp.conf b/images/webapp/supervisord_webapp.conf new file mode 100644 index 0000000000000000000000000000000000000000..387a55dc15babcb266d2b30124bccdb125d5434f --- /dev/null +++ b/images/webapp/supervisord_webapp.conf @@ -0,0 +1,17 @@ +[program:webapp] + +; Process definition +process_name = webapp +command = /etc/supervisor/conf.d/run_webapp.sh +autostart = true +autorestart = true +startsecs = 5 +stopwaitsecs = 10 +user = rosetta +environment =HOME=/rosetta + +; Log files +stdout_logfile = /var/log/webapp/startup.log +stdout_logfile_maxbytes = 100MB +stdout_logfile_backups = 100 +redirect_stderr = true diff --git a/rosetta/build b/rosetta/build index 347a2c139050aafe99cd531b4674f7876289e4b6..ab6de174d7048664c0453054b496a338c6642c38 100755 --- a/rosetta/build +++ b/rosetta/build @@ -35,7 +35,9 @@ if [[ "x$SERVICE" == "x" ]] ; then $BUILD_COMMAND images/slurmclustermaster -t rosetta/slurmclustermaster $BUILD_COMMAND images/slurmclusterworker -t rosetta/slurmclusterworker $BUILD_COMMAND images/dregistry -t rosetta/dregistry - + $BUILD_COMMAND images/webapp -t rosetta/webapp + + else # Build a specific image diff --git a/rosetta/setup b/rosetta/setup new file mode 100755 index 0000000000000000000000000000000000000000..13cd1389375cbc2d844f4c87113bd20f1828e28c --- /dev/null +++ b/rosetta/setup @@ -0,0 +1,9 @@ +#!/bin/bash + +# Use dev (local) database for backend +if [ ! -f images/webapp/db_conf.sh ]; then + echo "Using dev webapp database settings." + cp images/webapp/db_conf-dev.sh images/webapp/db_conf.sh +else + echo "Not using dev webapp database settings as settings are already present." +fi diff --git a/run_webapp_unit_tests.sh b/run_webapp_unit_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..b6f78879e7c33d26897f41981332fa90ed17b4ed --- /dev/null +++ b/run_webapp_unit_tests.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Example: ./run_webapp_unit_tests.sh rosetta.base_app.tests.test_apis.ApiTests.test_api_web_auth + +# You probably want to set DJANGO_LOG_LEVEL to ERROR and ROSETTA_LOG_LEVEL to DEBUG if you are doing tdd. +DJANGO_LOG_LEVEL="CRITICAL" +ROSETTA_LOG_LEVEL="CRITICAL" + +rosetta/shell webapp "cd /opt/webapp_code && DJANGO_LOG_LEVEL=$DJANGO_LOG_LEVEL ROSETTA_LOG_LEVEL=$ROSETTA_LOG_LEVEL python3 manage.py test $@" diff --git a/view_webapp_server_log.sh b/view_webapp_server_log.sh new file mode 100755 index 0000000000000000000000000000000000000000..0572279206088fc14d6077ea262ddbd5a109d0ac --- /dev/null +++ b/view_webapp_server_log.sh @@ -0,0 +1,2 @@ +#!/bin/bash +tail -f -n 1000 data_rosetta/webapp/log/server.log diff --git a/view_webapp_startup_log.sh b/view_webapp_startup_log.sh new file mode 100755 index 0000000000000000000000000000000000000000..5e57e64cb4a9dbd06874283765aedf6813259e58 --- /dev/null +++ b/view_webapp_startup_log.sh @@ -0,0 +1,2 @@ +#!/bin/bash +tail -f -n 1000 data_rosetta/webapp/log/startup.log