diff --git a/.gitignore b/.gitignore index 38eee2e6..34a5a41b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,10 @@ __pycache__ .idea .vscode - env* venv support +# openshift config check folder +app-config/openshift/test-cara +app-config/openshift/cara-prod diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e3ddd0b..f57bc054 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,8 @@ +stages: + - test + - docker-build + - oc-tag + - deploy # Use the acc-py-devtools templates found at # https://gitlab.cern.ch/-/ide/project/acc-co/devops/python/acc-py-devtools/blob/master/-/acc_py_devtools/templates/gitlab-ci/python.yml. @@ -10,6 +15,9 @@ variables: PY_VERSION: "3.9" +# ################################################################################################### +# Test code + # A full installation of CARA, tested with pytest. test_install: extends: .acc_py_full_test @@ -21,10 +29,20 @@ test_dev: # A development installation of CARA tested with pytest. +test_dev-39: + variables: + PY_VERSION: "3.9" + extends: .acc_py_dev_test + + +# ################################################################################################### +# Test OpenShift config + .test_openshift_config: + stage: test rules: - if: '$OC_TOKEN && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $BRANCH' - allow_failure: false # The branch must represent what is deployed. + allow_failure: true # The branch must represent what is deployed. FIXME: change to true because of a diff between ConfigMaps - if: '$OC_TOKEN && $CI_MERGE_REQUEST_EVENT_TYPE != "detached"' allow_failure: true # Anything other than the branch may fail without blocking the pipeline. image: registry.cern.ch/docker.io/mambaorg/micromamba @@ -34,7 +52,6 @@ test_dev: - wget https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - tar xzf ./openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - mv openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit/oc $HOME/env/bin/ - script: - cd ./app-config/openshift - oc login ${OC_SERVER} --token="${OC_TOKEN}" @@ -43,57 +60,51 @@ test_dev: - python ./config-normalise.py ./${CARA_INSTANCE}/actual ./${CARA_INSTANCE}/actual-normed - python ./config-normalise.py ./${CARA_INSTANCE}/expected ./${CARA_INSTANCE}/expected-normed - diff -u ./${CARA_INSTANCE}/actual-normed/ ./${CARA_INSTANCE}/expected-normed/ - artifacts: paths: - ./app-config/openshift/${CARA_INSTANCE}/actual - ./app-config/openshift/${CARA_INSTANCE}/expected -check_openshift_config_test-cara: +check_openshift_config_test: extends: .test_openshift_config variables: CARA_INSTANCE: 'test-cara' BRANCH: 'live/test-cara' - OC_SERVER: openshift-dev.cern.ch - OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_TEST_CARA}" + OC_SERVER: https://api.paas.okd.cern.ch + OC_TOKEN: "${OPENSHIFT_TEST_CONFIG_CHECKER_TOKEN}" check_openshift_config_prod: extends: .test_openshift_config variables: - CARA_INSTANCE: 'cara' + CARA_INSTANCE: 'cara-prod' BRANCH: 'master' - OC_SERVER: openshift.cern.ch - OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_PROD}" + OC_SERVER: https://api.paas.okd.cern.ch + OC_TOKEN: "${OPENSHIFT_PROD_CONFIG_CHECKER_TOKEN}" -# A development installation of CARA tested with pytest. -test_dev-39: - variables: - PY_VERSION: "3.9" - extends: .acc_py_dev_test - +# ################################################################################################### +# Build docker images .image_builder: - # Build and push images to the openshift instance, which automatically triggers an application re-deployment. - stage: deploy - image: - # Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image. - name: gitlab-registry.cern.ch/ci-tools/docker-image-builder - entrypoint: [""] - rules: - - if: '$OPENSHIFT_DOCKER_TOKEN_TEST != "" && $CI_COMMIT_BRANCH == "live/test-cara"' - variables: - DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_TEST}" - DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_TEST}" - - if: '$OPENSHIFT_DOCKER_TOKEN_PROD != "" && $CI_COMMIT_BRANCH == "master"' - variables: - DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_PROD}" - DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_PROD}" - script: - - echo "{\"auths\":{\"$DOCKER_REGISTRY\":{\"auth\":\"$DOCKER_TOKEN\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR/$DOCKER_CONTEXT_DIRECTORY --dockerfile $CI_PROJECT_DIR/$DOCKERFILE_DIRECTORY/Dockerfile --destination $DOCKER_REGISTRY/$IMAGE_NAME:latest + # Build and push images to the openshift instance, which automatically triggers an application re-deployment. + stage: docker-build + rules: + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + IMAGE_TAG: test-cara-latest + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + IMAGE_TAG: cara-prod-latest + image: + # Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image. + name: gitlab-registry.cern.ch/ci-tools/docker-image-builder + entrypoint: [""] + script: + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - echo "Building ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:latest Docker image..." + - /kaniko/executor --context ${CI_PROJECT_DIR}/${DOCKER_CONTEXT_DIRECTORY} --dockerfile ${CI_PROJECT_DIR}/${DOCKERFILE_DIRECTORY}/Dockerfile --destination ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG} auth-service-image_builder: @@ -114,32 +125,67 @@ cara-webservice-image_builder: DOCKER_CONTEXT_DIRECTORY: "" -trigger_build_on_openshift: - stage: deploy - rules: - - if: '$OPENSHIFT_BUILD_WEBHOOK_SECRET' - script: - - curl -X POST -k https://openshift.cern.ch:443/apis/build.openshift.io/v1/namespaces/cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_BUILD_WEBHOOK_SECRET}/generic - - -deploy_to_test: - stage: deploy - rules: - - if: '$CI_COMMIT_BRANCH == "live/test-cara" && $OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET' - script: - - curl -X POST -k https://openshift-dev.cern.ch:443/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic - - oci_calculator: - # A convenient way for users to run the CARA calculator. + extends: + - .image_builder + variables: + IMAGE_NAME: calculator + DOCKERFILE_DIRECTORY: app-config/cara-public-docker-image + DOCKER_CONTEXT_DIRECTORY: "" + + +# ################################################################################################### +# Link build Docker images OpenShift <-> GitLab registry + +.link_docker_images_with_gitlab_registry: + stage: oc-tag + image: gitlab-registry.cern.ch/paas-tools/openshift-client:latest + rules: + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + OC_PROJECT: "test-cara" + OC_TOKEN: ${OPENSHIFT_TEST_DEPLOY_TOKEN} + IMAGE_TAG: test-cara-latest + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + OC_PROJECT: "cara-prod" + OC_TOKEN: ${OPENSHIFT_PROD_DEPLOY_TOKEN} + IMAGE_TAG: cara-prod-latest + script: + - oc tag --source=docker ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest --token ${OC_TOKEN} --server=https://api.paas.okd.cern.ch -n ${OC_PROJECT} + +link_auth-service_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: auth-service + +link_cara-webservice_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: cara-webservice + +link_calculator_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: calculator + + +# ################################################################################################### +# Trigger build of CARA router on OpenShift + +trigger_cara-router_build_on_openshift: stage: deploy rules: - # Only run if branch is master (the default branch). - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - image: - name: gitlab-registry.cern.ch/ci-tools/docker-image-builder - entrypoint: [""] + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + OC_PROJECT: "test-cara" + BUILD_WEBHOOK_SECRET: ${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET} + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + OC_PROJECT: "cara-prod" + BUILD_WEBHOOK_SECRET: ${OPENSHIFT_PROD_BUILD_WEBHOOK_SECRET} script: - - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/app-config/cara-public-docker-image/Dockerfile --destination $CI_REGISTRY_IMAGE/calculator:latest - + - curl -X POST -k https://api.paas.okd.cern.ch/apis/build.openshift.io/v1/namespaces/${OC_PROJECT}/buildconfigs/cara-router/webhooks/${BUILD_WEBHOOK_SECRET}/generic diff --git a/README.md b/README.md index d6d770f9..832d39f2 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ python -m cara.apps.calculator To run with the CERN theme: ``` -python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern +python -m cara.apps.calculator --theme=cara/apps/templates/cern ``` To run the calculator on a different URL path: @@ -175,31 +175,33 @@ but it may be origin if you haven't configured it differently): First, get the [oc](https://docs.okd.io/3.11/cli_reference/get_started_cli.html) client and then login: ```console -$ oc login https://openshift-dev.cern.ch +$ oc login https://api.paas.okd.cern.ch ``` Then, switch to the project that you want to update: ```console -$ oc project test-cara +$ oc project cara-test ``` -If you need to create the application in a new project, run: +Create a new service account in OpenShift to use GitLab container registry: ```console -$ cd app-config/openshift +$ oc create serviceaccount gitlabci-deployer +serviceaccount "gitlabci-deployer" created -$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc create -f - -$ oc process -f configmap.yaml | oc create -f - -$ oc process -f services.yaml | oc create -f - -$ oc process -f imagestreams.yaml | oc create -f - -$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f - -$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc create -f - +$ oc policy add-role-to-user registry-editor -z gitlabci-deployer + +# We will refer to the output of this command as `test-token` +$ oc serviceaccounts get-token gitlabci-deployer +<...test-token...> ``` +Add the token to GitLab to allow GitLab to access OpenShift and define/change image stream tags. Go to `Settings` -> `CI / CD` -> `Variables` -> click on `Expand` button and create the variable `OPENSHIFT_TEST_DEPLOY_TOKEN`: insert the token `<...test-token...>`. + Then, create the webhook secret to be able to trigger automatic builds from GitLab. -Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_CARA_TEST_WEBHOOK_SECRET`. +Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_TEST_WEBHOOK_SECRET`. ```console $ WEBHOOKSECRET=$(openssl rand -hex 50) @@ -214,10 +216,25 @@ For CI usage, we also suggest creating a service account: oc create sa gitlab-config-checker ``` -Under ``Resources`` -> ``Membership`` enable the ``View`` role for this new service account. +Under ``User Management`` -> ``RoleBindings`` create a new `RoleBinding` to grant `View` access to the `gitlab-config-checker` service account: -To get this new user's authentication token go to ``Resources`` -> ``Secrets`` and locate the token in the newly -created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). +* name: `gitlab-config-checker-view-role` +* role name: `view` +* service account: `gitlab-config-checker` + +To get this new user's authentication token go to ``User Management`` -> ``Service Accounts`` -> `gitlab-config-checker` and locate the token in the newly created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). Copy the `token` value from `Data`. + +Create the various configurations: + +```console +$ cd app-config/openshift + +$ oc process -f configmap.yaml | oc create -f - +$ oc process -f services.yaml | oc create -f - +$ oc process -f imagestreams.yaml | oc create -f - +$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f - +$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc create -f - +``` ### CERN SSO integration @@ -269,11 +286,10 @@ $ cd app-config/openshift $ oc process -f configmap.yaml | oc replace -f - $ oc process -f services.yaml | oc replace -f - -$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc replace -f - $ oc process -f imagestreams.yaml | oc replace -f - $ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc replace -f - -$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc replace -f - +$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc replace -f - ``` -Be aware that if you change/replace the **route** of the PROD instance, -it will lose the annotation to be exposed outside CERN (not committed in this repo). +Be aware that if you create/recreate the environment you must manually create a **route** in OpenShift, +specifying the respective annotation to be exposed outside CERN. \ No newline at end of file diff --git a/app-config/auth-service/Dockerfile b/app-config/auth-service/Dockerfile index 3c726961..75ef9310 100644 --- a/app-config/auth-service/Dockerfile +++ b/app-config/auth-service/Dockerfile @@ -1,4 +1,4 @@ -FROM condaforge/mambaforge as conda +FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda RUN mamba create --yes -p /opt/app python=3.9 COPY . /opt/app-source @@ -17,7 +17,7 @@ RUN cd /opt/app \ && find /opt/app/lib -name '*.pyx' -delete \ ; -FROM debian +FROM registry.cern.ch/docker.io/library/debian COPY --from=conda /opt/app /opt/app CMD [ \ diff --git a/app-config/cara-public-docker-image/Dockerfile b/app-config/cara-public-docker-image/Dockerfile index d61f49d7..b4a01695 100644 --- a/app-config/cara-public-docker-image/Dockerfile +++ b/app-config/cara-public-docker-image/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM registry.cern.ch/docker.io/library/python:3.9 # Copy just the requirements.txt initially, allowing Docker effectively to cache the build (good for dev). COPY ./requirements.txt /tmp/requirements.txt diff --git a/app-config/cara-webservice/Dockerfile b/app-config/cara-webservice/Dockerfile index 517b30c8..a1a0ba1d 100644 --- a/app-config/cara-webservice/Dockerfile +++ b/app-config/cara-webservice/Dockerfile @@ -1,4 +1,4 @@ -FROM condaforge/mambaforge as conda +FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda RUN mamba create --yes -p /opt/app python=3.9 COPY . /opt/app-source @@ -18,7 +18,7 @@ RUN cd /opt/app \ && find /opt/app/lib -name '*.pyx' -delete \ ; -FROM debian +FROM registry.cern.ch/docker.io/library/debian COPY --from=conda /opt/app /opt/app ENV PATH=/opt/app/bin/:$PATH diff --git a/app-config/docker-compose.yml b/app-config/docker-compose.yml index 18d9d0a0..e96da5fd 100644 --- a/app-config/docker-compose.yml +++ b/app-config/docker-compose.yml @@ -12,7 +12,7 @@ services: - COOKIE_SECRET - APP_NAME=cara-webservice - CARA_CALCULATOR_PREFIX=/calculator-cern - - CARA_THEME=cara/apps/calculator/themes/cern + - CARA_THEME=cara/apps/templates/cern user: ${CURRENT_UID} cara-calculator-open: diff --git a/app-config/openshift/buildconfig.yaml b/app-config/openshift/buildconfig.yaml index 822050c0..9bcbc165 100644 --- a/app-config/openshift/buildconfig.yaml +++ b/app-config/openshift/buildconfig.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-application" creationTimestamp: null @@ -12,7 +12,7 @@ objects: - kind: BuildConfig - apiVersion: v1 + apiVersion: build.openshift.io/v1 metadata: name: cara-router labels: diff --git a/app-config/openshift/config-fetch.py b/app-config/openshift/config-fetch.py index 55a27230..c596c31c 100644 --- a/app-config/openshift/config-fetch.py +++ b/app-config/openshift/config-fetch.py @@ -9,7 +9,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.description = "Fetch the openshift config for CARA" parser.set_defaults(handler=handler) parser.add_argument( - "instance", choices=['cara', 'test-cara'], + "instance", choices=['cara-prod', 'test-cara'], help="Pick the instance for which you want to fetch the config", ) parser.add_argument( @@ -35,25 +35,32 @@ def get_oc_server() -> typing.Optional[str]: def fetch_config(output_directory: pathlib.Path): output_directory.mkdir(exist_ok=True, parents=True) - for component in ['routes', 'configmap', 'services', 'imagestreams', 'buildconfig', 'deploymentconfig']: + for component, name in [ + ('configmap', 'auth-service'), + ('services', None), + ('imagestreams', None), + ('buildconfig', None), + ('deploymentconfig', None)]: + with (output_directory / f'{component}.yaml').open('wt') as fh: - cmd = ['oc', 'get', '--export', '-o', 'yaml', component] + cmd = ['oc', 'get', '-o', 'yaml', component] + if name: + cmd += [name] print(f'Running: {" ".join(cmd)}') subprocess.run(cmd, stdout=fh, check=True) print(f'Config in: {output_directory.absolute()}') def handler(args: argparse.ArgumentParser) -> None: - if args.instance == 'cara': - login_server = 'https://openshift.cern.ch:443' - project_name = 'cara' + login_server = 'https://api.paas.okd.cern.ch:443' + if args.instance == 'cara-prod': + project_name = 'cara-prod' elif args.instance == 'test-cara': - login_server = 'https://openshift-dev.cern.ch:443' project_name = 'test-cara' actual_login_server = get_oc_server() if actual_login_server != login_server: - print(f'\nPlease login to the correct openshift server with: \n\n oc login {login_server}\n', file=sys.stderr) + print(f'\nPlease login to the correct OpenShift server with: \n\n oc login {login_server}\n', file=sys.stderr) sys.exit(1) subprocess.run(['oc', 'project', project_name], stdout=subprocess.DEVNULL, check=True) diff --git a/app-config/openshift/config-generate.py b/app-config/openshift/config-generate.py index c17c411a..3a33db5b 100644 --- a/app-config/openshift/config-generate.py +++ b/app-config/openshift/config-generate.py @@ -8,7 +8,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.description = "Generate the config files which can be later submitted to openshift" parser.set_defaults(handler=handler) parser.add_argument( - "instance", choices=['cara', 'test-cara'], + "instance", choices=['cara-prod', 'test-cara'], help="Pick the instance for which you want to generate the config", ) parser.add_argument( @@ -28,7 +28,6 @@ def generate_config(output_directory: pathlib.Path, project_name: str, hostname: print(f'Running: {" ".join(cmd)}') subprocess.run(cmd, stdout=fh, check=True) - oc_process('routes', context={'HOST': hostname}) oc_process('configmap') oc_process('services') oc_process('imagestreams') @@ -39,13 +38,13 @@ def generate_config(output_directory: pathlib.Path, project_name: str, hostname: def handler(args: argparse.ArgumentParser) -> None: - if args.instance == 'cara': - project_name = 'cara' + if args.instance == 'cara-prod': + project_name = 'cara-prod' branch = 'master' hostname = 'cara.web.cern.ch' elif args.instance == 'test-cara': - branch = 'live/test-cara' project_name = 'test-cara' + branch = 'live/test-cara' hostname = 'test-cara.web.cern.ch' generate_config(pathlib.Path(args.output_directory), project_name, hostname, branch) diff --git a/app-config/openshift/config-normalise.py b/app-config/openshift/config-normalise.py index 4672a128..e9ea6c70 100644 --- a/app-config/openshift/config-normalise.py +++ b/app-config/openshift/config-normalise.py @@ -22,8 +22,9 @@ def clean_ephemeral_config(config: dict): config.get('metadata', []).clear() METADATA_TO_PRESERVE = ['labels', 'name'] + CERN_OKD4_METADATA_LABELS = ['migration.openshift.io', 'velero.io'] - for item in config['items']: + for item in config.get('items', {}): item.pop('status', None) for key in list(item['metadata'].keys()): @@ -31,10 +32,14 @@ def clean_ephemeral_config(config: dict): del item['metadata'][key] item.get('spec', {}).pop('clusterIP', None) + item.get('spec', {}).pop('clusterIPs', None) + item.get('spec', {}).pop('revisionHistoryLimit', None) if item['kind'] == 'BuildConfig': for trigger in item.get('spec', {}).get('triggers', []): trigger.get('imageChange', {}).pop('lastTriggeredImageID', None) + item.get('spec', {}).pop('failedBuildsHistoryLimit', None) + item.get('spec', {}).pop('successfulBuildsHistoryLimit', None) if item['kind'] == 'DeploymentConfig': item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None) @@ -46,6 +51,11 @@ def clean_ephemeral_config(config: dict): for trigger in item['spec'].get('triggers', []): trigger.get('imageChangeParams', {}).pop('lastTriggeredImage', None) + for label in list(item['metadata'].get('labels', {}).keys()): + for prefix in CERN_OKD4_METADATA_LABELS: + if label.startswith(prefix): + item['metadata']['labels'].pop(label) + # Drop the template part of the config for now. # TODO: Remove this constraint to ensure our deployments reflect the fact that they are templated. r = item['metadata'].get('labels', {}).pop('template', None) diff --git a/app-config/openshift/configmap.yaml b/app-config/openshift/configmap.yaml index be119b92..9b6e2cbf 100644 --- a/app-config/openshift/configmap.yaml +++ b/app-config/openshift/configmap.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-configuration" annotations: diff --git a/app-config/openshift/deploymentconfig.yaml b/app-config/openshift/deploymentconfig.yaml index fcdd97e1..e4260f94 100644 --- a/app-config/openshift/deploymentconfig.yaml +++ b/app-config/openshift/deploymentconfig.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-application" annotations: @@ -10,7 +10,7 @@ template: "cara-application" objects: - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: auth-service @@ -69,7 +69,7 @@ name: 'auth-service:latest' namespace: ${PROJECT_NAME} - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-app @@ -126,7 +126,7 @@ name: 'cara-webservice:latest' namespace: ${PROJECT_NAME} - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-router @@ -179,7 +179,7 @@ namespace: ${PROJECT_NAME} - type: ConfigChange - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-webservice @@ -208,7 +208,7 @@ - name: CARA_CALCULATOR_PREFIX value: /calculator-cern - name: CARA_THEME - value: cara/apps/calculator/themes/cern + value: cara/apps/templates/cern image: '${PROJECT_NAME}/cara-webservice' ports: - containerPort: 8080 @@ -263,7 +263,7 @@ namespace: ${PROJECT_NAME} - type: ConfigChange - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-calculator-open diff --git a/app-config/openshift/imagestreams.yaml b/app-config/openshift/imagestreams.yaml index 26754cd4..5a3205c5 100644 --- a/app-config/openshift/imagestreams.yaml +++ b/app-config/openshift/imagestreams.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-imagestreams" creationTimestamp: null @@ -12,7 +12,7 @@ objects: - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: auth-service spec: @@ -20,7 +20,7 @@ local: False - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: cara-router spec: @@ -28,7 +28,7 @@ local: False - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: cara-webservice spec: diff --git a/app-config/openshift/routes.yaml b/app-config/openshift/routes.example.yaml similarity index 90% rename from app-config/openshift/routes.yaml rename to app-config/openshift/routes.example.yaml index 70874b9b..a7f0f0e1 100644 --- a/app-config/openshift/routes.yaml +++ b/app-config/openshift/routes.example.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-route" creationTimestamp: null @@ -11,7 +11,7 @@ template: "cara-route" objects: - - apiVersion: v1 + apiVersion: route.openshift.io/v1 kind: Route metadata: name: cara-route diff --git a/app-config/openshift/services.yaml b/app-config/openshift/services.yaml index 73b53a07..d5de132e 100644 --- a/app-config/openshift/services.yaml +++ b/app-config/openshift/services.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-services" creationTimestamp: null diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index 407382e3..6bfcb9d7 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -33,7 +33,7 @@ from .user import AuthenticatedUser, AnonymousUser # calculator version. If the calculator needs to make breaking changes (e.g. change # form attributes) then it can also increase its MAJOR version without needing to # increase the overall CARA version (found at ``cara.__version__``). -__version__ = "3.2.0" +__version__ = "3.3.0" class BaseRequestHandler(RequestHandler): @@ -148,11 +148,13 @@ class StaticModel(BaseRequestHandler): class LandingPage(BaseRequestHandler): def get(self): + template_environment = self.settings["template_environment"] template = self.settings["template_environment"].get_template( "index.html.j2") report = template.render( user=self.current_user, calculator_prefix=self.settings["calculator_prefix"], + text_blocks=template_environment.globals['common_text'] ) self.finish(report) @@ -229,7 +231,7 @@ def make_app( calculator_templates = Path(__file__).parent / "templates" templates_directories = [cara_templates, calculator_templates] if theme_dir: - templates_directories.insert(0, theme_dir / 'templates') + templates_directories.insert(0, theme_dir) loader = jinja2.FileSystemLoader([str(path) for path in templates_directories]) template_environment = jinja2.Environment( loader=loader, diff --git a/cara/apps/calculator/__main__.py b/cara/apps/calculator/__main__.py index 7ec61206..7ec8b69a 100644 --- a/cara/apps/calculator/__main__.py +++ b/cara/apps/calculator/__main__.py @@ -36,7 +36,6 @@ def main(): if theme_dir is not None: theme_dir = Path(theme_dir).absolute() assert theme_dir.exists() - assert (theme_dir / 'templates').exists() app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir) app.listen(args.port) IOLoop.instance().start() diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 06c29229..d237a3fc 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -303,7 +303,7 @@ class FormData: # Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise if self.ventilation_type == 'natural_ventilation': if self.window_opening_regime == 'windows_open_periodically': - window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration) + window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, min(self.infected_start, self.exposed_start)) else: window_interval = always_on @@ -694,7 +694,7 @@ MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply', MASK_TYPES = {'Type I', 'FFP2'} MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'} -VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_B117', 'SARS_CoV_2_P1', 'SARS_CoV_2_B16172'} +VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_B117', 'SARS_CoV_2_B1351','SARS_CoV_2_P1', 'SARS_CoV_2_B16172', 'SARS_CoV_2_B11529'} VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'} WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'} WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'} diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index a4d88bd9..e703e225 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -308,10 +308,10 @@ class ReportGenerator: context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form) context['calculator_prefix'] = self.calculator_prefix context['scale_warning'] = { - 'level': 'yellow-2', - 'incidence_rate': 'lower than 25 new cases per 100 000 inhabitants', - 'onsite_access': 'of about 8000', - 'threshold': '' + 'level': 'red-4', + 'incidence_rate': 'higher or equal to 100 new cases per 100 000 inhabitants', + 'onsite_access': 'lower than 4000', + 'threshold': '5%' } return context diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js index 71510e8c..5a20f025 100644 --- a/cara/apps/calculator/static/js/form.js +++ b/cara/apps/calculator/static/js/form.js @@ -236,6 +236,20 @@ function on_ventilation_type_change() { }); } +function on_wearing_mask_change() { + wearing_mask = $('input[type=radio][name=mask_wearing_option]') + wearing_mask.each(function (index) { + if (this.checked) { + getChildElement($(this)).show(); + require_fields(this); + } + else { + getChildElement($(this)).hide(); + require_fields(this); + } + }) +} + /* -------UI------- */ function show_disclaimer() { @@ -558,6 +572,12 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_ventilation_type_change(); + // When the mask_wearing_option changes we want to make its respective + // children show/hide. + $("input[type=radio][name=mask_wearing_option]").change(on_wearing_mask_change); + // Call the function now to handle forward/back button presses in the browser. + on_wearing_mask_change(); + // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. setMaxInfectedPeople(); diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 874922e6..13ce0b93 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -154,16 +154,16 @@ class ExposureModelResult(View): def update_textual_result(self, model: models.ExposureModel): lines = [] - P = model.infection_probability() + P = np.array(model.infection_probability()).mean() # lines.append(f'Emission rate (virus/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}') lines.append(f'Probability of infection: {np.round(P, 0)}%') lines.append(f'Number of exposed: {model.exposed.number}') - new_cases = np.round(model.expected_new_cases(), 1) + new_cases = np.round(np.array(model.expected_new_cases(), 1).mean()) lines.append(f'Number of expected new cases: {new_cases}') - R0 = np.round(model.reproduction_number(), 1) + R0 = np.round(np.array(model.reproduction_number(), 1).mean()) lines.append(f'Reproduction number (R0): {R0}') self.html_output.value = '
\n'.join(lines) diff --git a/cara/apps/static/css/style.css b/cara/apps/static/css/style.css index 008f833a..022b4ca6 100644 --- a/cara/apps/static/css/style.css +++ b/cara/apps/static/css/style.css @@ -170,6 +170,10 @@ body { display: block; } +.mask_icons { + height: 4em; +} + /*===== FIXED BACKGROUND IMG =====*/ .fixed-background { @@ -230,6 +234,10 @@ footer img { width: 25%; } + #mobile_calculator_option { + display: none; + } + #nat_vent_image { height: 15em; } @@ -242,7 +250,7 @@ footer img { height: 6em; margin: 1%; } - #mobile-app-buttons { + #calculator_app_button { display: none!important; } .feedback { @@ -282,6 +290,9 @@ footer img { .nav-link { padding: .5rem .5rem!important; } + #apps_dropdown { + display: none; + } #report_version { font-size: .5rem; } @@ -298,13 +309,6 @@ footer img { #mobile_link { display: inline!important; } - #desktop_logo { - display: none!important; - } - - #mobile_logo { - display: block!important; - } .feedback { float:right; font-size:.75rem; @@ -324,16 +328,16 @@ footer img { } */ -/* Large (lg) devices (desktops, 992px and up) */ -@media (max-width: 992px) { - #download-pdf { +/* Large (lg) devices (tablets) */ +@media (max-width: 64em) { + .expert_app_button { display: none; } - #link_reproduce_results { - display: none; + #desktop_logo { + display: none!important; } - #mobile_link { - display: inline!important; + #mobile_logo { + display: block!important; } } diff --git a/cara/apps/static/images/masks/ffp2.png b/cara/apps/static/images/masks/ffp2.png new file mode 100644 index 00000000..57de5fc1 Binary files /dev/null and b/cara/apps/static/images/masks/ffp2.png differ diff --git a/cara/apps/static/images/masks/t1.png b/cara/apps/static/images/masks/t1.png new file mode 100644 index 00000000..187031f3 Binary files /dev/null and b/cara/apps/static/images/masks/t1.png differ diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/templates/base/calculator.form.html.j2 similarity index 91% rename from cara/apps/calculator/templates/calculator.form.html.j2 rename to cara/apps/templates/base/calculator.form.html.j2 index 75cbbe5b..5ec3caed 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/templates/base/calculator.form.html.j2 @@ -63,13 +63,15 @@
-
-
+
+
@@ -77,9 +79,11 @@
Room data: -
+ {% block room_data %} +
?
+ {% endblock room_data %}
@@ -188,7 +192,7 @@
-
+
@@ -197,7 +201,7 @@
-
+
@@ -205,11 +209,11 @@
@@ -260,15 +264,30 @@
Are masks worn when occupants are at workstations?
- + - +
- Type of masks used: - - - -
+ +
@@ -282,7 +301,7 @@
-
+
@@ -290,7 +309,7 @@
- +
@@ -305,7 +324,7 @@ - +
@@ -499,9 +518,11 @@ Virus data:
SARS-CoV-2 covers the original "wild type" strain of the virus and three variants of concern (VOC):
Modify the default as necessary, according to local area prevalence e.g. for Geneva or Ain (France).
@@ -528,8 +549,8 @@
  • Library = all seated, no talking, just breathing,
  • Laboratory = light physical activity, talking 50% of the time,
  • Workshop = moderate physical activity, talking 50% of the time,
  • -
  • Training = trainer standing and talking, rest seated and talking quietly. - Trainer assumed infected (worst case scenario),
  • +
  • Conference/Training = speaker/trainer standing and talking, rest seated and talking quietly. + Speaker/Trainer assumed infected (worst case scenario),
  • Gym = heavy exercise, no talking, just breathing.
  • Activity breaks:
    diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/templates/base/calculator.report.html.j2 similarity index 97% rename from cara/apps/calculator/templates/base/calculator.report.html.j2 rename to cara/apps/templates/base/calculator.report.html.j2 index d6d1e893..d6df2b41 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/templates/base/calculator.report.html.j2 @@ -21,7 +21,7 @@
    -

    CARA - CALCULATOR REPORT

    +

    REPORT

    Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}

    @@ -202,10 +202,14 @@ SARS-CoV-2 (nominal strain) {% elif form.virus_type == "SARS_CoV_2_B117" %} SARS-CoV-2 (Alpha VOC) + {% elif form.virus_type == "SARS_CoV_2_B1351" %} + SARS-CoV-2 (Beta VOC) {% elif form.virus_type == "SARS_CoV_2_P1" %} SARS-CoV-2 (Gamma VOC) {% elif form.virus_type == "SARS_CoV_2_B16172" %} SARS-CoV-2 (Delta VOC) + {% elif form.virus_type == "SARS_CoV_2_B11529" %} + SARS-CoV-2 (Omicron VOC) {% endif %}

  • Room Volume: {{ model.concentration_model.room.volume }} m³

  • @@ -298,7 +302,7 @@ {% elif form.activity_type == "workshop" %} Workshop = assembly workshop environment, all persons doing moderate physical activity, talking 50% of the time. {% elif form.activity_type == "training" %} - Training – one person (the trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the trainer is the infected person, for the worst case scenario. + Conference/Training – one person (the speaker/trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the speaker/trainer is the infected person, for the worst case scenario. {% elif form.activity_type == "lab" %} Laboratory = Lab or technical environment, all persons doing light physical activity, talking 50% of the time. {% elif form.activity_type == "gym" %} diff --git a/cara/apps/templates/base/index.html.j2 b/cara/apps/templates/base/index.html.j2 new file mode 100644 index 00000000..3df8598f --- /dev/null +++ b/cara/apps/templates/base/index.html.j2 @@ -0,0 +1,59 @@ +{% extends "layout.html.j2" %} +{% set active_page="home/" %} + +{% block main %} + {#
    #} +
    +
    + + +
    +
    + +
    + +
    +
    +

    Introduction


    +
    +

    + CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. + It does this by simulating the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming homogenous mixing, and it estimates the risk of COVID-19 airborne transmission therein. + Please see the About page for more details on the methodology, assumptions and limitations of CARA. +

    +

    + The full CARA source code can be accessed freely under an Apache 2.0 open source license from our code repository. + It includes detailed instructions on how to run your own version of this tool. +

    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    Apps:

    +
    + +
    + +
    +
    +
    + + {% block cara_at_cern %} + {% endblock cara_at_cern %} + +
    +

    Acknowledgements


    + {{ text_blocks['Acknowledgements'] }} + +
    +
    +{% endblock main %} diff --git a/cara/apps/templates/base/layout.html.j2 b/cara/apps/templates/base/layout.html.j2 new file mode 100644 index 00000000..cfa9bb09 --- /dev/null +++ b/cara/apps/templates/base/layout.html.j2 @@ -0,0 +1,120 @@ + + + + + + + + + + {% block title %} + CARA | COVID Airborne Risk Assessment + {% endblock title %} + + + + + + + + {% block extra_headers %} + {% endblock extra_headers %} + + + + + + +
    + {% block main %} + {% endblock main %} +
    + + + + + + + + + + + + + + + + {% block body_scripts %} + {% endblock body_scripts %} + + + diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/templates/base/userguide.html.j2 similarity index 95% rename from cara/apps/calculator/templates/userguide.html.j2 rename to cara/apps/templates/base/userguide.html.j2 index a7f99c9e..e94e7514 100644 --- a/cara/apps/calculator/templates/userguide.html.j2 +++ b/cara/apps/templates/base/userguide.html.j2 @@ -70,8 +70,10 @@ The choices are:

    The user can modify the selected variant from the default, according to the prevalence of the different variants in the local area. Access to this information can be found here:


    Timings

    @@ -210,7 +214,7 @@ If not, then you can input separate breaks. This is particularly different when

    The model allows for a simulation with either a continuous wearing of face masks throughout the duration of the event, or have the removed at all times - i.e. all occupants (infected and exposed alike) wear or not masks for the duration of the simulation. Please bear in mind the user inputs shall be aligned with the current applicable public health & safety instructions. Please check what are the applicable rules, before deciding which assumptions are used for the simulation.

    -

    If you have selected the Training activity type, this equates to the trainer and all participants either wearing masks throughout the training (Yes), or removing them when seated/standing at their socially distanced positions within the training room (No). +

    If you have selected the Conference/Training activity type, this equates to the speakr/trainer and all participants either wearing masks throughout the conference/training (Yes), or removing them when seated/standing at their socially distanced positions within the conference/training room (No). Please confirm what are the applicable rules, before deciding which assumptions are used for the simulation

    For the time being only the Type 1 surgical and FFP2 masks can be selected.


    diff --git a/cara/apps/templates/calculator.form.html.j2 b/cara/apps/templates/calculator.form.html.j2 new file mode 100644 index 00000000..368da022 --- /dev/null +++ b/cara/apps/templates/calculator.form.html.j2 @@ -0,0 +1,2 @@ +{# The main calculator form, this template is intended to be implemented by themes #} +{% extends "base/calculator.form.html.j2" %} diff --git a/cara/apps/calculator/templates/calculator.report.html.j2 b/cara/apps/templates/calculator.report.html.j2 similarity index 100% rename from cara/apps/calculator/templates/calculator.report.html.j2 rename to cara/apps/templates/calculator.report.html.j2 diff --git a/cara/apps/templates/cern/calculator.form.html.j2 b/cara/apps/templates/cern/calculator.form.html.j2 new file mode 100644 index 00000000..79a879d9 --- /dev/null +++ b/cara/apps/templates/cern/calculator.form.html.j2 @@ -0,0 +1,7 @@ +{% extends "base/calculator.form.html.j2" %} + +{% block room_data %} +
    + ? +
    +{% endblock room_data %} \ No newline at end of file diff --git a/cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 b/cara/apps/templates/cern/calculator.report.html.j2 similarity index 100% rename from cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 rename to cara/apps/templates/cern/calculator.report.html.j2 diff --git a/cara/apps/templates/cern/index.html.j2 b/cara/apps/templates/cern/index.html.j2 new file mode 100644 index 00000000..4ad3ef16 --- /dev/null +++ b/cara/apps/templates/cern/index.html.j2 @@ -0,0 +1,11 @@ +{% extends "base/index.html.j2" %} + +{% block cara_at_cern %} +

    CARA @ CERN


    +
    +

    + CARA has been developed by CERN with the intention of allowing members of personnel with roles related to supervision, health & safety or space management to simulate the concerned workplaces on CERN sites. + A hosted CERN version of the CARA Covid Calculator is available on this site to members of the CERN personnel. +

    +
    +{% endblock cara_at_cern %} \ No newline at end of file diff --git a/cara/apps/templates/cern/layout.html.j2 b/cara/apps/templates/cern/layout.html.j2 new file mode 100644 index 00000000..3959e097 --- /dev/null +++ b/cara/apps/templates/cern/layout.html.j2 @@ -0,0 +1,5 @@ +{% extends "base/layout.html.j2" %} + +{% block covid_information %} + +{% endblock covid_information %} \ No newline at end of file diff --git a/cara/apps/templates/cern/userguide.html.j2 b/cara/apps/templates/cern/userguide.html.j2 new file mode 100644 index 00000000..28ab09db --- /dev/null +++ b/cara/apps/templates/cern/userguide.html.j2 @@ -0,0 +1,5 @@ +{% extends "base/userguide.html.j2" %} + +{% block room_volume_guide %} +This information is available via GIS Portal (https://gis.cern.ch/gisportal/).

    +{% endblock room_volume_guide %} \ No newline at end of file diff --git a/cara/apps/templates/index.html.j2 b/cara/apps/templates/index.html.j2 index 30ed7f07..87cbd196 100644 --- a/cara/apps/templates/index.html.j2 +++ b/cara/apps/templates/index.html.j2 @@ -1,69 +1,2 @@ -{% extends "layout.html.j2" %} -{% set active_page="home/" %} - -{% block main %} - {#
    #} -
    -
    - - -
    -
    - -
    -
    -
    Calculator
    -
    Expert (beta)
    -
    -
    -
    -

    Introduction


    -
    -

    - CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. - It does this by simulating the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming homogenous mixing, and it estimates the risk of COVID-19 airborne transmission therein. - Please see the About page for more details on the methodology, assumptions and limitations of CARA. -

    -

    - The full CARA source code can be accessed freely under an Apache 2.0 open source license from our code repository. - It includes detailed instructions on how to run your own version of this tool. -

    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -

    Apps:

    -
    - -
    -
    Expert (beta)
    -
    -
    -
    - -

    CARA @ CERN


    -
    -

    - CARA has been developed by CERN with the intention of allowing members of personnel with roles related to supervision, health & safety or space management to simulate the concerned workplaces on CERN sites. - A hosted CERN version of the CARA Covid Calculator is available on this site to members of the CERN personnel. -

    -
    -
    -

    Acknowledgements


    -

    - We wish to thank CERN’s HSE Unit, Beams Department, Experimental Physics Department, Information Technology Department, Industry, Procurement and Knowledge Transfer Department and International Relations Sector for their support to the study. - Thanks to Doris Forkel-Wirth, Benoit Delille, Walid Fadel, Olga Beltramello, Letizia Di Giulio, Evelyne Dho, Wayne Salter, Benoit Salvant and colleagues from the COVID working group for providing expert advice and extensively testing the model. - Finally, we wish to thank Fabienne Landua and the design service for preparing the illustrations and Alessandro Raimondo, Ana Padua and Manuela Cirilli from the Knowledge Transfer Group for their continuous support. - Our compliments towards the work and research performed by world leading scientists in this domain: Dr. Julian Tang, Prof. Manuel Gameiro, Dr. Linsey Marr, Prof. Jose Jimenez, Prof. Lidia Morawska, Prof. Yuguo Li et al. - their scientific contribution was indispensable for this project. - -

    -
    - -{% endblock main %} +{# The main index, this template is intended to be implemented by themes #} +{% extends "base/index.html.j2" %} \ No newline at end of file diff --git a/cara/apps/templates/layout.html.j2 b/cara/apps/templates/layout.html.j2 index 15020daa..1298f8eb 100644 --- a/cara/apps/templates/layout.html.j2 +++ b/cara/apps/templates/layout.html.j2 @@ -1,113 +1,2 @@ - - - - - - - - - - {% block title %} - CARA | COVID Airborne Risk Assessment - {% endblock title %} - - - - - - - - {% block extra_headers %} - {% endblock extra_headers %} - - - - - - -
    - {% block main %} - {% endblock main %} -
    - - - - - - - - - - - - - - - - {% block body_scripts %} - {% endblock body_scripts %} - - - +{# The main layout, this template is intended to be implemented by themes #} +{% extends "base/layout.html.j2" %} diff --git a/cara/apps/templates/userguide.html.j2 b/cara/apps/templates/userguide.html.j2 new file mode 100644 index 00000000..6b34bc4d --- /dev/null +++ b/cara/apps/templates/userguide.html.j2 @@ -0,0 +1,2 @@ +{# The main calculator report, this template is intended to be implemented by themes #} +{% extends "base/userguide.html.j2" %} \ No newline at end of file diff --git a/cara/models.py b/cara/models.py index 272b6b47..8fdbd34b 100644 --- a/cara/models.py +++ b/cara/models.py @@ -123,11 +123,14 @@ class PeriodicInterval(Interval): #: occurring, a value of 0 signifies that the event never happens. duration: float + #: Time at which the first person (infected or exposed) arrives at the enclosed space. + start: float = 0.0 + def boundaries(self) -> BoundarySequence_t: if self.period == 0 or self.duration == 0: return tuple() result = [] - for i in np.arange(0, 24, self.period / 60): + for i in np.arange(self.start, 24, self.period / 60): # NOTE: It is important that the time type is float, not np.float, in # order to allow hashability (for caching). result.append((float(i), float(i+self.duration/60))) @@ -450,11 +453,8 @@ class SARSCoV2(Virus): piecewise constant model (for more details see A. Henriques et al, CERN-OPEN-2021-004, DOI: 10.17181/CERN.1GDQ.5Y75) """ - halflife = np.empty_like(humidity) - halflife[humidity <= 0.4] = 3.8 - halflife[humidity > 0.4] = 1.1 - return halflife - + return np.piecewise(humidity, [humidity <= 0.4, humidity > 0.4], [3.8, 1.1]) + Virus.types = { 'SARS_CoV_2': SARSCoV2( @@ -478,6 +478,10 @@ Virus.types = { viral_load_in_sputum=1e9, infectious_dose=30/1.6, ), + 'SARS_CoV_2_B11529': SARSCoV2( + viral_load_in_sputum=1e9, + infectious_dose=50/4.9841, + ), } diff --git a/cara/monte_carlo/data.py b/cara/monte_carlo/data.py index 4415efbb..330d21ab 100644 --- a/cara/monte_carlo/data.py +++ b/cara/monte_carlo/data.py @@ -40,6 +40,8 @@ symptomatic_vl_frequencies = LogCustomKernel( # From CERN-OPEN-2021-04 and refererences therein +# NB the infectious dose is inversely proportional to infectiousness of the strain +# i.e. an infectious dose of 80 is 25% more infectious than a dose of 100, (1/(80/100)) virus_distributions = { 'SARS_CoV_2': mc.SARSCoV2( viral_load_in_sputum=symptomatic_vl_frequencies, @@ -49,6 +51,10 @@ virus_distributions = { viral_load_in_sputum=symptomatic_vl_frequencies, infectious_dose=60, ), + 'SARS_CoV_2_B1351': mc.SARSCoV2( + viral_load_in_sputum=symptomatic_vl_frequencies, + infectious_dose=80, + ), 'SARS_CoV_2_P1': mc.SARSCoV2( viral_load_in_sputum=symptomatic_vl_frequencies, infectious_dose=100/2.25, @@ -56,7 +62,11 @@ virus_distributions = { 'SARS_CoV_2_B16172': mc.SARSCoV2( viral_load_in_sputum=symptomatic_vl_frequencies, infectious_dose=60/1.6, - ), + ), + 'SARS_CoV_2_B11529': mc.SARSCoV2( + viral_load_in_sputum=symptomatic_vl_frequencies, + infectious_dose=(1/4.9841)*100, + ), } diff --git a/cara/scripts/themes/cern/cara_script.command b/cara/scripts/themes/cern/cara_script.command index 0d957e2f..952d852a 100755 --- a/cara/scripts/themes/cern/cara_script.command +++ b/cara/scripts/themes/cern/cara_script.command @@ -12,4 +12,4 @@ pip3 install -e . echo "############################################" echo "CARA is now running at http://localhost:8080" echo "############################################" -python3 -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern \ No newline at end of file +python3 -m cara.apps.calculator --theme=cara/apps/templates/cern \ No newline at end of file diff --git a/cara/scripts/themes/cern/cara_script.sh b/cara/scripts/themes/cern/cara_script.sh index dcd21d3f..4a25af3e 100755 --- a/cara/scripts/themes/cern/cara_script.sh +++ b/cara/scripts/themes/cern/cara_script.sh @@ -6,4 +6,4 @@ pip install -e . echo "############################################" echo "CARA is now running at http://localhost:8080" echo "############################################" -python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern \ No newline at end of file +python -m cara.apps.calculator --theme=cara/apps/templates/cern \ No newline at end of file diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index 6ee821c0..dd635946 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -48,7 +48,7 @@ def test_ventilation_slidingwindow(baseline_form: model_generator.FormData): assert isinstance(baseline_window, models.SlidingWindow) window = models.SlidingWindow( - active=models.PeriodicInterval(period=120, duration=10), + active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=baseline_window.outside_temp, window_height=1.6, opening_length=0.6, @@ -80,7 +80,7 @@ def test_ventilation_hingedwindow(baseline_form: model_generator.FormData): assert isinstance(baseline_window, models.HingedWindow) window = models.HingedWindow( - active=models.PeriodicInterval(period=120, duration=10), + active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=baseline_window.outside_temp, window_height=1.6, window_width=1., opening_length=0.6, @@ -141,7 +141,7 @@ def test_ventilation_window_hepa(baseline_form: model_generator.FormData): # Now build the equivalent ventilation instance directly, and compare. window = models.SlidingWindow( - active=models.PeriodicInterval(period=120, duration=10), + active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=baseline_window.outside_temp, window_height=1.6, opening_length=0.6, diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index 09bc104c..65f3b216 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -72,14 +72,13 @@ class TestBasicApp(tornado.testing.AsyncHTTPTestCase): class TestCernApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): - cern_theme = Path(cara.apps.calculator.__file__).parent / 'themes' / 'cern' + cern_theme = Path(cara.apps.calculator.__file__).parent.parent / 'themes' / 'cern' return cara.apps.calculator.make_app(theme_dir=cern_theme) @tornado.testing.gen_test(timeout=_TIMEOUT) def test_report(self): response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) - assert 'CERN HSE' in response.body.decode() assert 'expected number of new cases is' in response.body.decode() diff --git a/server-performance-tests/README.md b/server-performance-tests/README.md new file mode 100644 index 00000000..7bf5e43b --- /dev/null +++ b/server-performance-tests/README.md @@ -0,0 +1,18 @@ +# locust + +A simple open source load testing tool that allows to define user behavior. + +In order to set it up for the first time, we followed the documentation at https://locust.io/. In particular, we: + +* Defined a class for the users that will be simulating. +* Defined a ``wait_time`` variable that will make the simulated users wait between the specified seconds after each task executed. +* Decorated our method with ``@Task`` that creates a micro-thread that calls this method. +* Defined the ``self.client`` attribute that makes it possible to make HTTP calls that will be logged by Locust. + +To use, uncomment the desired method on ``lucust.py``` file, open the terminal on this folder and run the following command: + +``locust -f locust.py --host https://cara.web.cern.ch`` + +Then, open up a browser and point it to http://localhost:8089. +By default we pointed out the test to our own web server. +``Start swarming`` will trigger the simulation. \ No newline at end of file diff --git a/server-performance-tests/locust.py b/server-performance-tests/locust.py new file mode 100644 index 00000000..1cee4ae0 --- /dev/null +++ b/server-performance-tests/locust.py @@ -0,0 +1,42 @@ +from locust import HttpUser, task, between +from gevent.pool import Group +import time + +''' +Method no. 1 - Simulation with each single user +running x requests in parallel. +Specify the desired number of parallel requests in +"num_of_parallel_requests" variable (35 by default). + +This method was used in simulations with one single +user perfoming 35 requests in parallel. +''' +# num_of_parallel_requests = 35 +# class User(HttpUser): +# wait_time = between(0.05, 0.1) + +# @task(1) +# def test_api(self): +# group = Group() +# for i in range(0, num_of_parallel_requests): +# group.spawn(lambda:self.client.get("/calculator-open/baseline-model/result")) +# group.join() +# while(1): +# time.sleep(1) + +''' +Method no. 2 - Simulation with different users +running x requests concurrently. +With this method, each user is intended to +perform one single request. + +This method was used in simulations with different +number of users requesting once at the same time. +''' +# class User(HttpUser): + +# @task(1) +# def test_api(self): +# self.client.get("/calculator-open/baseline-model/result") +# while(1): +# time.sleep(1) \ No newline at end of file