From a76545d600efcd6baea1d4e170fc8360382588c4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 9 Jul 2020 15:41:58 +0200 Subject: Update to Gradle 6.5.1 This commit updates the Gradle wrapper to version 6.5.1 to address some of the issues we were having when importing the project. --- .gitattributes | 6 ++- simulator/build.gradle.kts | 4 -- simulator/gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 58910 bytes simulator/gradle/wrapper/gradle-wrapper.properties | 2 +- simulator/gradlew | 53 +++++++++++++-------- simulator/gradlew.bat | 22 ++++++++- 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/.gitattributes b/.gitattributes index 526c8a38..9d6ef431 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ -*.sh text eol=lf \ No newline at end of file +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf +*.sh text eol=lf diff --git a/simulator/build.gradle.kts b/simulator/build.gradle.kts index 90f43749..4775369b 100644 --- a/simulator/build.gradle.kts +++ b/simulator/build.gradle.kts @@ -30,7 +30,3 @@ allprojects { group = "com.atlarge.opendc" version = "2.0.0" } - -tasks.wrapper { - gradleVersion = "6.0" -} diff --git a/simulator/gradle/wrapper/gradle-wrapper.jar b/simulator/gradle/wrapper/gradle-wrapper.jar index 87b738cb..62d4c053 100644 Binary files a/simulator/gradle/wrapper/gradle-wrapper.jar and b/simulator/gradle/wrapper/gradle-wrapper.jar differ diff --git a/simulator/gradle/wrapper/gradle-wrapper.properties b/simulator/gradle/wrapper/gradle-wrapper.properties index a4b44297..bb8b2fc2 100644 --- a/simulator/gradle/wrapper/gradle-wrapper.properties +++ b/simulator/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/simulator/gradlew b/simulator/gradlew index af6708ff..fbd7c515 100755 --- a/simulator/gradlew +++ b/simulator/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/simulator/gradlew.bat b/simulator/gradlew.bat index 6d57edc7..5093609d 100644 --- a/simulator/gradlew.bat +++ b/simulator/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -65,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% -- cgit v1.2.3 From ba99e8f03ba8605deccac12b8c91cab94509dd94 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 9 Jul 2020 15:43:09 +0200 Subject: Remove unnecessary dotfiles This change removes configuration files (e.g. Travis CI and Gitlab CI) in the simulator directory which have become unnecessary due to the migration to a monorepo. --- simulator/.gitattributes | 7 ------- simulator/.gitlab-ci.yml | 34 ---------------------------------- simulator/.travis.yml | 1 - 3 files changed, 42 deletions(-) delete mode 100644 simulator/.gitattributes delete mode 100644 simulator/.gitlab-ci.yml delete mode 100644 simulator/.travis.yml diff --git a/simulator/.gitattributes b/simulator/.gitattributes deleted file mode 100644 index 12924725..00000000 --- a/simulator/.gitattributes +++ /dev/null @@ -1,7 +0,0 @@ -# https://help.github.com/articles/dealing-with-line-endings/ -# -# These are explicitly windows files and should use crlf -*.bat text eol=crlf - -# See https://github.com/gradle/gradle/issues/12248 -buildSrc/src/main/**/*.gradle.kts text eol=lf diff --git a/simulator/.gitlab-ci.yml b/simulator/.gitlab-ci.yml deleted file mode 100644 index a095f7e7..00000000 --- a/simulator/.gitlab-ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -image: gradle:6.1-jdk13 - -variables: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" - -before_script: - - export GRADLE_USER_HOME=`pwd`/.gradle - -stages: - - build - - test - -build: - stage: build - script: - - gradle --build-cache assemble - allow_failure: false - cache: - key: "$CI_COMMIT_REF_NAME" - policy: push - paths: - - build - - .gradle - -test: - stage: test - script: - - gradle check - cache: - key: "$CI_COMMIT_REF_NAME" - policy: pull - paths: - - build - - .gradle diff --git a/simulator/.travis.yml b/simulator/.travis.yml deleted file mode 100644 index dff5f3a5..00000000 --- a/simulator/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: java -- cgit v1.2.3 From 1a4776636bf6b585d4a19a6721d9d57b02c88ca4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 14 Jul 2020 19:51:44 +0200 Subject: Migrate from links to docker-compose networks This change migrates the docker-compose configuration from using links to using custom networks since links have been deprecated for some time. --- docker-compose.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6338e3d0..b2072be9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: restart: on-failure ports: - "8081:8081" - links: - - mongo + networks: + - backend depends_on: - mongo environment: @@ -56,6 +56,8 @@ services: - OPENDC_DB - OPENDC_DB_USERNAME - OPENDC_DB_PASSWORD + networks: + - backend # Comment out for public deployment ports: - 27017:27017 @@ -66,10 +68,10 @@ services: mongo-express: image: mongo-express restart: on-failure - links: - - mongo + networks: + - backend depends_on: - - mongo + - mongo ports: - 8082:8081 environment: @@ -79,3 +81,6 @@ services: volumes: mongo-volume: external: false + +networks: + backend: {} -- cgit v1.2.3 From 02997b2522b9c66072b16f1425c02e81e0085e3c Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 14 Jul 2020 21:10:56 +0200 Subject: Rename web-server to API This change renames the web-server component to API in order to be more descriptive of its role. The OpenDC API bridges between the frontend on one side and the database and simulator on the other side. --- .github/workflows/api.yml | 34 ++ .github/workflows/web-server.yml | 34 -- Dockerfile | 25 - README.md | 2 +- api/.gitignore | 15 + api/.gitlab-ci.yml | 26 + api/.pylintrc | 521 +++++++++++++++++++++ api/.style.yapf | 3 + api/Dockerfile | 17 + api/README.md | 101 ++++ api/check.sh | 1 + api/conftest.py | 15 + api/format.sh | 1 + api/main.py | 197 ++++++++ .../opendc-web-server-component-diagram.png | Bin 0 -> 90161 bytes api/opendc/__init__.py | 0 api/opendc/api/__init__.py | 0 api/opendc/api/v2/__init__.py | 0 api/opendc/api/v2/paths.json | 18 + api/opendc/api/v2/portfolios/__init__.py | 0 .../api/v2/portfolios/portfolioId/__init__.py | 0 .../api/v2/portfolios/portfolioId/endpoint.py | 65 +++ .../portfolios/portfolioId/scenarios/__init__.py | 0 .../portfolios/portfolioId/scenarios/endpoint.py | 43 ++ .../portfolioId/scenarios/test_endpoint.py | 119 +++++ .../api/v2/portfolios/portfolioId/test_endpoint.py | 149 ++++++ api/opendc/api/v2/prefabs/__init__.py | 0 api/opendc/api/v2/prefabs/endpoint.py | 23 + api/opendc/api/v2/prefabs/prefabId/__init__.py | 0 api/opendc/api/v2/prefabs/prefabId/endpoint.py | 0 .../api/v2/prefabs/prefabId/test_endpoint.py | 0 api/opendc/api/v2/prefabs/test_endpoint.py | 22 + api/opendc/api/v2/projects/__init__.py | 0 api/opendc/api/v2/projects/endpoint.py | 32 ++ api/opendc/api/v2/projects/projectId/__init__.py | 0 .../projects/projectId/authorizations/__init__.py | 0 .../projects/projectId/authorizations/endpoint.py | 17 + .../projectId/authorizations/test_endpoint.py | 40 ++ api/opendc/api/v2/projects/projectId/endpoint.py | 66 +++ .../v2/projects/projectId/portfolios/__init__.py | 0 .../v2/projects/projectId/portfolios/endpoint.py | 35 ++ .../projects/projectId/portfolios/test_endpoint.py | 83 ++++ .../api/v2/projects/projectId/test_endpoint.py | 119 +++++ .../v2/projects/projectId/topologies/__init__.py | 0 .../v2/projects/projectId/topologies/endpoint.py | 31 ++ .../projects/projectId/topologies/test_endpoint.py | 50 ++ api/opendc/api/v2/projects/test_endpoint.py | 23 + api/opendc/api/v2/scenarios/__init__.py | 0 api/opendc/api/v2/scenarios/scenarioId/__init__.py | 0 api/opendc/api/v2/scenarios/scenarioId/endpoint.py | 57 +++ .../api/v2/scenarios/scenarioId/test_endpoint.py | 140 ++++++ api/opendc/api/v2/schedulers/__init__.py | 0 api/opendc/api/v2/schedulers/endpoint.py | 9 + api/opendc/api/v2/schedulers/test_endpoint.py | 2 + api/opendc/api/v2/topologies/__init__.py | 0 .../api/v2/topologies/topologyId/__init__.py | 0 .../api/v2/topologies/topologyId/endpoint.py | 56 +++ .../api/v2/topologies/topologyId/test_endpoint.py | 116 +++++ api/opendc/api/v2/traces/__init__.py | 0 api/opendc/api/v2/traces/endpoint.py | 10 + api/opendc/api/v2/traces/test_endpoint.py | 6 + api/opendc/api/v2/traces/traceId/__init__.py | 0 api/opendc/api/v2/traces/traceId/endpoint.py | 14 + api/opendc/api/v2/traces/traceId/test_endpoint.py | 13 + api/opendc/api/v2/users/__init__.py | 0 api/opendc/api/v2/users/endpoint.py | 30 ++ api/opendc/api/v2/users/test_endpoint.py | 34 ++ api/opendc/api/v2/users/userId/__init__.py | 0 api/opendc/api/v2/users/userId/endpoint.py | 59 +++ api/opendc/api/v2/users/userId/test_endpoint.py | 53 +++ api/opendc/models/__init__.py | 0 api/opendc/models/model.py | 59 +++ api/opendc/models/portfolio.py | 24 + api/opendc/models/prefab.py | 26 + api/opendc/models/project.py | 31 ++ api/opendc/models/scenario.py | 26 + api/opendc/models/topology.py | 27 ++ api/opendc/models/trace.py | 7 + api/opendc/models/user.py | 36 ++ api/opendc/util/__init__.py | 0 api/opendc/util/database.py | 92 ++++ api/opendc/util/exceptions.py | 64 +++ api/opendc/util/parameter_checker.py | 85 ++++ api/opendc/util/path_parser.py | 39 ++ api/opendc/util/rest.py | 141 ++++++ api/pytest.ini | 5 + api/requirements.txt | 15 + api/static/index.html | 22 + docker-compose.yml | 6 +- web-server/.gitignore | 15 - web-server/.gitlab-ci.yml | 26 - web-server/.pylintrc | 521 --------------------- web-server/.style.yapf | 3 - web-server/README.md | 101 ---- web-server/check.sh | 1 - web-server/conftest.py | 15 - web-server/format.sh | 1 - web-server/main.py | 196 -------- .../opendc-web-server-component-diagram.png | Bin 90161 -> 0 bytes web-server/opendc/__init__.py | 0 web-server/opendc/api/__init__.py | 0 web-server/opendc/api/v2/__init__.py | 0 web-server/opendc/api/v2/paths.json | 18 - web-server/opendc/api/v2/portfolios/__init__.py | 0 .../api/v2/portfolios/portfolioId/__init__.py | 0 .../api/v2/portfolios/portfolioId/endpoint.py | 65 --- .../portfolios/portfolioId/scenarios/__init__.py | 0 .../portfolios/portfolioId/scenarios/endpoint.py | 43 -- .../portfolioId/scenarios/test_endpoint.py | 119 ----- .../api/v2/portfolios/portfolioId/test_endpoint.py | 149 ------ web-server/opendc/api/v2/prefabs/__init__.py | 0 web-server/opendc/api/v2/prefabs/endpoint.py | 23 - .../opendc/api/v2/prefabs/prefabId/__init__.py | 0 .../opendc/api/v2/prefabs/prefabId/endpoint.py | 53 --- .../api/v2/prefabs/prefabId/test_endpoint.py | 140 ------ web-server/opendc/api/v2/prefabs/test_endpoint.py | 22 - web-server/opendc/api/v2/projects/__init__.py | 0 web-server/opendc/api/v2/projects/endpoint.py | 32 -- .../opendc/api/v2/projects/projectId/__init__.py | 0 .../projects/projectId/authorizations/__init__.py | 0 .../projects/projectId/authorizations/endpoint.py | 17 - .../projectId/authorizations/test_endpoint.py | 40 -- .../opendc/api/v2/projects/projectId/endpoint.py | 66 --- .../v2/projects/projectId/portfolios/__init__.py | 0 .../v2/projects/projectId/portfolios/endpoint.py | 35 -- .../projects/projectId/portfolios/test_endpoint.py | 83 ---- .../api/v2/projects/projectId/test_endpoint.py | 119 ----- .../v2/projects/projectId/topologies/__init__.py | 0 .../v2/projects/projectId/topologies/endpoint.py | 31 -- .../projects/projectId/topologies/test_endpoint.py | 50 -- web-server/opendc/api/v2/projects/test_endpoint.py | 23 - web-server/opendc/api/v2/scenarios/__init__.py | 0 .../opendc/api/v2/scenarios/scenarioId/__init__.py | 0 .../opendc/api/v2/scenarios/scenarioId/endpoint.py | 57 --- .../api/v2/scenarios/scenarioId/test_endpoint.py | 140 ------ web-server/opendc/api/v2/schedulers/__init__.py | 0 web-server/opendc/api/v2/schedulers/endpoint.py | 9 - .../opendc/api/v2/schedulers/test_endpoint.py | 2 - web-server/opendc/api/v2/topologies/__init__.py | 0 .../api/v2/topologies/topologyId/__init__.py | 0 .../api/v2/topologies/topologyId/endpoint.py | 56 --- .../api/v2/topologies/topologyId/test_endpoint.py | 116 ----- web-server/opendc/api/v2/traces/__init__.py | 0 web-server/opendc/api/v2/traces/endpoint.py | 10 - web-server/opendc/api/v2/traces/test_endpoint.py | 6 - .../opendc/api/v2/traces/traceId/__init__.py | 0 .../opendc/api/v2/traces/traceId/endpoint.py | 14 - .../opendc/api/v2/traces/traceId/test_endpoint.py | 13 - web-server/opendc/api/v2/users/__init__.py | 0 web-server/opendc/api/v2/users/endpoint.py | 30 -- web-server/opendc/api/v2/users/test_endpoint.py | 34 -- web-server/opendc/api/v2/users/userId/__init__.py | 0 web-server/opendc/api/v2/users/userId/endpoint.py | 59 --- .../opendc/api/v2/users/userId/test_endpoint.py | 53 --- web-server/opendc/models/__init__.py | 0 web-server/opendc/models/model.py | 59 --- web-server/opendc/models/portfolio.py | 24 - web-server/opendc/models/prefab.py | 26 - web-server/opendc/models/project.py | 31 -- web-server/opendc/models/scenario.py | 26 - web-server/opendc/models/topology.py | 27 -- web-server/opendc/models/trace.py | 7 - web-server/opendc/models/user.py | 36 -- web-server/opendc/util/__init__.py | 0 web-server/opendc/util/database.py | 92 ---- web-server/opendc/util/exceptions.py | 64 --- web-server/opendc/util/parameter_checker.py | 85 ---- web-server/opendc/util/path_parser.py | 39 -- web-server/opendc/util/rest.py | 141 ------ web-server/pytest.ini | 5 - web-server/requirements.txt | 15 - web-server/static/index.html | 22 - 172 files changed, 3168 insertions(+), 3368 deletions(-) create mode 100644 .github/workflows/api.yml delete mode 100644 .github/workflows/web-server.yml delete mode 100644 Dockerfile create mode 100644 api/.gitignore create mode 100644 api/.gitlab-ci.yml create mode 100644 api/.pylintrc create mode 100644 api/.style.yapf create mode 100644 api/Dockerfile create mode 100644 api/README.md create mode 100755 api/check.sh create mode 100644 api/conftest.py create mode 100755 api/format.sh create mode 100755 api/main.py create mode 100644 api/misc/artwork/opendc-web-server-component-diagram.png create mode 100644 api/opendc/__init__.py create mode 100644 api/opendc/api/__init__.py create mode 100644 api/opendc/api/v2/__init__.py create mode 100644 api/opendc/api/v2/paths.json create mode 100644 api/opendc/api/v2/portfolios/__init__.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/__init__.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/endpoint.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py create mode 100644 api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py create mode 100644 api/opendc/api/v2/prefabs/__init__.py create mode 100644 api/opendc/api/v2/prefabs/endpoint.py create mode 100644 api/opendc/api/v2/prefabs/prefabId/__init__.py create mode 100644 api/opendc/api/v2/prefabs/prefabId/endpoint.py create mode 100644 api/opendc/api/v2/prefabs/prefabId/test_endpoint.py create mode 100644 api/opendc/api/v2/prefabs/test_endpoint.py create mode 100644 api/opendc/api/v2/projects/__init__.py create mode 100644 api/opendc/api/v2/projects/endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/__init__.py create mode 100644 api/opendc/api/v2/projects/projectId/authorizations/__init__.py create mode 100644 api/opendc/api/v2/projects/projectId/authorizations/endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/portfolios/__init__.py create mode 100644 api/opendc/api/v2/projects/projectId/portfolios/endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/test_endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/topologies/__init__.py create mode 100644 api/opendc/api/v2/projects/projectId/topologies/endpoint.py create mode 100644 api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py create mode 100644 api/opendc/api/v2/projects/test_endpoint.py create mode 100644 api/opendc/api/v2/scenarios/__init__.py create mode 100644 api/opendc/api/v2/scenarios/scenarioId/__init__.py create mode 100644 api/opendc/api/v2/scenarios/scenarioId/endpoint.py create mode 100644 api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py create mode 100644 api/opendc/api/v2/schedulers/__init__.py create mode 100644 api/opendc/api/v2/schedulers/endpoint.py create mode 100644 api/opendc/api/v2/schedulers/test_endpoint.py create mode 100644 api/opendc/api/v2/topologies/__init__.py create mode 100644 api/opendc/api/v2/topologies/topologyId/__init__.py create mode 100644 api/opendc/api/v2/topologies/topologyId/endpoint.py create mode 100644 api/opendc/api/v2/topologies/topologyId/test_endpoint.py create mode 100644 api/opendc/api/v2/traces/__init__.py create mode 100644 api/opendc/api/v2/traces/endpoint.py create mode 100644 api/opendc/api/v2/traces/test_endpoint.py create mode 100644 api/opendc/api/v2/traces/traceId/__init__.py create mode 100644 api/opendc/api/v2/traces/traceId/endpoint.py create mode 100644 api/opendc/api/v2/traces/traceId/test_endpoint.py create mode 100644 api/opendc/api/v2/users/__init__.py create mode 100644 api/opendc/api/v2/users/endpoint.py create mode 100644 api/opendc/api/v2/users/test_endpoint.py create mode 100644 api/opendc/api/v2/users/userId/__init__.py create mode 100644 api/opendc/api/v2/users/userId/endpoint.py create mode 100644 api/opendc/api/v2/users/userId/test_endpoint.py create mode 100644 api/opendc/models/__init__.py create mode 100644 api/opendc/models/model.py create mode 100644 api/opendc/models/portfolio.py create mode 100644 api/opendc/models/prefab.py create mode 100644 api/opendc/models/project.py create mode 100644 api/opendc/models/scenario.py create mode 100644 api/opendc/models/topology.py create mode 100644 api/opendc/models/trace.py create mode 100644 api/opendc/models/user.py create mode 100644 api/opendc/util/__init__.py create mode 100644 api/opendc/util/database.py create mode 100644 api/opendc/util/exceptions.py create mode 100644 api/opendc/util/parameter_checker.py create mode 100644 api/opendc/util/path_parser.py create mode 100644 api/opendc/util/rest.py create mode 100644 api/pytest.ini create mode 100644 api/requirements.txt create mode 100644 api/static/index.html delete mode 100644 web-server/.gitignore delete mode 100644 web-server/.gitlab-ci.yml delete mode 100644 web-server/.pylintrc delete mode 100644 web-server/.style.yapf delete mode 100644 web-server/README.md delete mode 100755 web-server/check.sh delete mode 100644 web-server/conftest.py delete mode 100755 web-server/format.sh delete mode 100644 web-server/main.py delete mode 100644 web-server/misc/artwork/opendc-web-server-component-diagram.png delete mode 100644 web-server/opendc/__init__.py delete mode 100644 web-server/opendc/api/__init__.py delete mode 100644 web-server/opendc/api/v2/__init__.py delete mode 100644 web-server/opendc/api/v2/paths.json delete mode 100644 web-server/opendc/api/v2/portfolios/__init__.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/__init__.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/prefabs/__init__.py delete mode 100644 web-server/opendc/api/v2/prefabs/endpoint.py delete mode 100644 web-server/opendc/api/v2/prefabs/prefabId/__init__.py delete mode 100644 web-server/opendc/api/v2/prefabs/prefabId/endpoint.py delete mode 100644 web-server/opendc/api/v2/prefabs/prefabId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/prefabs/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/authorizations/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/authorizations/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/topologies/__init__.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/topologies/endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/projectId/topologies/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/projects/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/scenarios/__init__.py delete mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/__init__.py delete mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py delete mode 100644 web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/schedulers/__init__.py delete mode 100644 web-server/opendc/api/v2/schedulers/endpoint.py delete mode 100644 web-server/opendc/api/v2/schedulers/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/topologies/__init__.py delete mode 100644 web-server/opendc/api/v2/topologies/topologyId/__init__.py delete mode 100644 web-server/opendc/api/v2/topologies/topologyId/endpoint.py delete mode 100644 web-server/opendc/api/v2/topologies/topologyId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/traces/__init__.py delete mode 100644 web-server/opendc/api/v2/traces/endpoint.py delete mode 100644 web-server/opendc/api/v2/traces/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/traces/traceId/__init__.py delete mode 100644 web-server/opendc/api/v2/traces/traceId/endpoint.py delete mode 100644 web-server/opendc/api/v2/traces/traceId/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/users/__init__.py delete mode 100644 web-server/opendc/api/v2/users/endpoint.py delete mode 100644 web-server/opendc/api/v2/users/test_endpoint.py delete mode 100644 web-server/opendc/api/v2/users/userId/__init__.py delete mode 100644 web-server/opendc/api/v2/users/userId/endpoint.py delete mode 100644 web-server/opendc/api/v2/users/userId/test_endpoint.py delete mode 100644 web-server/opendc/models/__init__.py delete mode 100644 web-server/opendc/models/model.py delete mode 100644 web-server/opendc/models/portfolio.py delete mode 100644 web-server/opendc/models/prefab.py delete mode 100644 web-server/opendc/models/project.py delete mode 100644 web-server/opendc/models/scenario.py delete mode 100644 web-server/opendc/models/topology.py delete mode 100644 web-server/opendc/models/trace.py delete mode 100644 web-server/opendc/models/user.py delete mode 100644 web-server/opendc/util/__init__.py delete mode 100644 web-server/opendc/util/database.py delete mode 100644 web-server/opendc/util/exceptions.py delete mode 100644 web-server/opendc/util/parameter_checker.py delete mode 100644 web-server/opendc/util/path_parser.py delete mode 100644 web-server/opendc/util/rest.py delete mode 100644 web-server/pytest.ini delete mode 100644 web-server/requirements.txt delete mode 100644 web-server/static/index.html diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml new file mode 100644 index 00000000..7335c737 --- /dev/null +++ b/.github/workflows/api.yml @@ -0,0 +1,34 @@ +name: REST API + +on: + push: + paths: + - 'api/*' + +defaults: + run: + working-directory: api + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python: [3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with pylint + run: | + ./check.sh + - name: Test with pytest + run: | + pytest opendc diff --git a/.github/workflows/web-server.yml b/.github/workflows/web-server.yml deleted file mode 100644 index 6f14f97b..00000000 --- a/.github/workflows/web-server.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Web server - -on: - push: - paths: - - 'web-server/*' - -defaults: - run: - working-directory: web-server - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - python: [3.8] - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Lint with pylint - run: | - ./check.sh - - name: Test with pytest - run: | - pytest opendc diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 50af30b1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM nikolaik/python-nodejs:python3.8-nodejs14 -MAINTAINER OpenDC Maintainers - -## Dockerfile for the frontend/server part of the deployment - -# Installing packages -RUN apt-get update \ - && apt-get install -y yarn git sed - -# Copy OpenDC directory -COPY ./ /opendc - -# Fetch web server dependencies -RUN pip install -r /opendc/web-server/requirements.txt - -# Build frontend -RUN cd /opendc/frontend \ - && rm -rf ./build \ - && yarn \ - && yarn build - -# Set working directory -WORKDIR /opendc - -CMD ["sh", "-c", "python web-server/main.py"] diff --git a/README.md b/README.md index 14156eff..66ffd22d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ OpenDC is a project by the [@Large Research Group](http://atlarge-research.com). ## Architecture -OpenDC consists of four components: a Kotlin [simulator](/simulator), a MongoDB database, a Python Flask [web server](/web-server), and a React.js [frontend](/frontend), each in their own subdirectories. +OpenDC consists of four components: a Kotlin [simulator](/simulator), a MongoDB database, a Python Flask [web server](/api), and a React.js [frontend](/frontend), each in their own subdirectories.

OpenDC Component Diagram diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 00000000..fef0da65 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +*.pyc +*.pyo +venv +venv* +dist +build +*.egg +*.egg-info +_mailinglist +.tox +.cache/ +.idea/ +config.json +test.json diff --git a/api/.gitlab-ci.yml b/api/.gitlab-ci.yml new file mode 100644 index 00000000..d80ba836 --- /dev/null +++ b/api/.gitlab-ci.yml @@ -0,0 +1,26 @@ +image: "python:3.8" + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +cache: + paths: + - .cache/pip + +stages: + - static-analysis + - test + +static-analysis: + stage: static-analysis + script: + - python --version + - pip install -r requirements.txt + - pylint opendc + +test: + stage: test + script: + - python --version + - pip install -r requirements.txt + - pytest opendc diff --git a/api/.pylintrc b/api/.pylintrc new file mode 100644 index 00000000..f25e4fc2 --- /dev/null +++ b/api/.pylintrc @@ -0,0 +1,521 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=duplicate-code, + missing-module-docstring, + invalid-name, + bare-except, + too-few-public-methods + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=12 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/api/.style.yapf b/api/.style.yapf new file mode 100644 index 00000000..f5c26c57 --- /dev/null +++ b/api/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style = pep8 +column_limit=120 diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 00000000..49702c90 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.8 +MAINTAINER OpenDC Maintainers + +# Ensure the STDOUT is not buffered by Python so that our logs become visible +# See https://stackoverflow.com/q/29663459/10213073 +ENV PYTHONUNBUFFERED 1 + +# Copy OpenDC directory +COPY ./ /opendc + +# Fetch web server dependencies +RUN pip install -r /opendc/requirements.txt + +# Set working directory +WORKDIR /opendc + +CMD ["python3", "main.py"] diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..4e8110d0 --- /dev/null +++ b/api/README.md @@ -0,0 +1,101 @@ +

+ OpenDC +
+ OpenDC Web Server +

+

+ Collaborative Datacenter Simulation and Exploration for Everybody +

+ +
+ +The OpenDC web server is the bridge between OpenDC's frontend and database. It is built with Flask/SocketIO in Python and implements the OpenAPI-compliant [OpenDC API specification](../opendc-api-spec.yml). + +This document explains a high-level view of the web server architecture ([jump](#architecture)), and describes how to set up the web server for local development ([jump](#setup-for-local-development)). + +## Architecture + +The following diagram shows a high-level view of the architecture of the OpenDC web server. Squared-off colored boxes indicate packages (colors become more saturated as packages are nested); rounded-off boxes indicate individual components; dotted lines indicate control flow; and solid lines indicate data flow. + +![OpenDC Web Server Component Diagram](misc/artwork/opendc-web-server-component-diagram.png) + +The OpenDC API is implemented by the `Main Server Loop`, which is the only component in the base package. + +### Util Package + +The `Util` package handles several miscellaneous tasks: + +* `Database API`: Wraps database access functionality used by `Models` to read themselves from/write themselves into the database. +* `Exceptions`: Holds definitions for exceptions used throughout the web server. +* `Parameter Checker`: Recursively checks whether required `Request` parameters are present and correctly typed. +* `REST`: Parses SocketIO and HTTP messages into `Request` objects, and calls the appropriate `API` endpoint to get a `Response` object to return to the `Main Server Loop`. + +### API Package + +The `API` package contains the logic for the HTTP methods in each API endpoint. Packages are structured to mirror the API: the code for the endpoint `GET api/projects`, for example, would be located at the `endpoint.py` inside the `projects` package (so at `api/projects/endpoint.py`). + +An `endpoint.py` file contains methods for each HTTP method it supports, which takes a request as input (such as `def GET(request):`). Typically, such a method checks whether the parameters were passed correctly (using the `Parameter Checker`); fetches some model from the database; checks whether the data exists and is accessible by the user who made the request; possibly modifies this data and writes it back to the database; and returns a JSON representation of the model. + +The `REST` component dynamically imports the appropriate method from the appropriate `endpoint`, according to request it receives, and executes it. + +### Models Package + +The `models` package contains the logic for mapping Python objects to their database representations. This involves an abstract `model` which has generic CRUD operations. Extensions of `model`, such as a `User` or `Project`, specify some more specific operations and their collection metadata. + +`Endpoint`s import these `models` and use them to execute requests. + +## Setup for Local Development + +The following steps will guide you through setting up the OpenDC web server locally for development. To test individual endpoints, edit `static/index.html`. + +### Local Setup + +#### Install requirements + +Make sure you have Python 3.7+ installed (if not, get it [here](https://www.python.org/)), as well as pip (if not, get it [here](https://pip.pypa.io/en/stable/installing/)). Then run the following to install the requirements. + +```bash +pip install -r requirements.txt +``` + +The web server also requires a running MongoDB instance. We recommend setting this up through docker, by running `docker-compose build` and `docker-compose up` in the [`mongodb` directory](../database) of the main OpenDC repository. + +#### Get and configure the code + +Clone OpenDC and follow the [instructions in the main repository](../) to set up a Google OAuth ID and environment variables. + +**Important:** Be sure to set up environment variables according to those instructions, in a `.env` file. + +In `api/static/index.html`, add your own `OAUTH_CLIENT_ID` in `content=` on line `2`. + +#### Set up the database + +You can selectively run only the database services from the standard OpenDC `docker-compose` setup: + +```bash +docker-compose build mongo mongo-express +docker-compose up mongo mongo-express +``` + +This will set you up with a running MongoDB instance and a visual inspection tool running on [localhost:8082](http://localhost:8082), with which you can view and manipulate the database. + +### Local Development + +Run the server. + +```bash +cd api +python main.py +``` + +When editing the web server code, restart the server (`CTRL` + `c` followed by `python main.py` in the console running the server) to see the result of your changes. + +#### Code Style + +To format all files, run `format.sh` in this directory (uses `yapf` internally). + +To check if code style is up to modern standards, run `check.sh` in this directory (uses `pylint` internally). + +#### Testing + +Run `pytest` in this directory to run all tests. diff --git a/api/check.sh b/api/check.sh new file mode 100755 index 00000000..abe2c596 --- /dev/null +++ b/api/check.sh @@ -0,0 +1 @@ +pylint opendc --ignore-patterns=test_.*?py diff --git a/api/conftest.py b/api/conftest.py new file mode 100644 index 00000000..1f4831b8 --- /dev/null +++ b/api/conftest.py @@ -0,0 +1,15 @@ +""" +Configuration file for all unit tests. +""" +import pytest + +from main import FLASK_CORE_APP + + +@pytest.fixture +def client(): + """Returns a Flask API client to interact with.""" + FLASK_CORE_APP.config['TESTING'] = True + + with FLASK_CORE_APP.test_client() as client: + yield client diff --git a/api/format.sh b/api/format.sh new file mode 100755 index 00000000..18cba452 --- /dev/null +++ b/api/format.sh @@ -0,0 +1 @@ +yapf **/*.py -i diff --git a/api/main.py b/api/main.py new file mode 100755 index 00000000..a2481269 --- /dev/null +++ b/api/main.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +import flask_socketio +import json +import os +import sys +import traceback +import urllib.request +from flask import Flask, request, send_from_directory, jsonify +from flask_compress import Compress +from oauth2client import client, crypt +from flask_cors import CORS +from dotenv import load_dotenv + +from opendc.models.user import User +from opendc.util import rest, path_parser, database +from opendc.util.exceptions import AuthorizationTokenError, RequestInitializationError + +load_dotenv() + +TEST_MODE = "OPENDC_FLASK_TESTING" in os.environ + +# Specify the directory of static assets +if TEST_MODE: + STATIC_ROOT = os.curdir +else: + STATIC_ROOT = os.path.join(os.environ['OPENDC_ROOT_DIR'], 'frontend', 'build') + +# Set up database if not testing +if not TEST_MODE: + database.DB.initialize_database( + user=os.environ['OPENDC_DB_USERNAME'], + password=os.environ['OPENDC_DB_PASSWORD'], + database=os.environ['OPENDC_DB'], + host=os.environ['OPENDC_DB_HOST'] if 'OPENDC_DB_HOST' in os.environ else 'localhost') + +# Set up the core app +FLASK_CORE_APP = Flask(__name__, static_url_path='', static_folder=STATIC_ROOT) +FLASK_CORE_APP.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] + +# Set up CORS support for local setups +if 'localhost' in os.environ['OPENDC_SERVER_BASE_URL']: + CORS(FLASK_CORE_APP) + +compress = Compress() +compress.init_app(FLASK_CORE_APP) + +if 'OPENDC_SERVER_BASE_URL' in os.environ or 'localhost' in os.environ['OPENDC_SERVER_BASE_URL']: + SOCKET_IO_CORE = flask_socketio.SocketIO(FLASK_CORE_APP, cors_allowed_origins="*") +else: + SOCKET_IO_CORE = flask_socketio.SocketIO(FLASK_CORE_APP) + + +@FLASK_CORE_APP.errorhandler(404) +def page_not_found(e): + return send_from_directory(STATIC_ROOT, 'index.html') + + +@FLASK_CORE_APP.route('/tokensignin', methods=['POST']) +def sign_in(): + """Authenticate a user with Google sign in""" + + try: + token = request.form['idtoken'] + except KeyError: + return 'No idtoken provided', 401 + + try: + idinfo = client.verify_id_token(token, os.environ['OPENDC_OAUTH_CLIENT_ID']) + + if idinfo['aud'] != os.environ['OPENDC_OAUTH_CLIENT_ID']: + raise crypt.AppIdentityError('Unrecognized client.') + + if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: + raise crypt.AppIdentityError('Wrong issuer.') + except ValueError: + url = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={}".format(token) + req = urllib.request.Request(url) + response = urllib.request.urlopen(url=req, timeout=30) + res = response.read() + idinfo = json.loads(res) + except crypt.AppIdentityError as e: + return 'Did not successfully authenticate' + + user = User.from_google_id(idinfo['sub']) + + data = {'isNewUser': user.obj is None} + + if user.obj is not None: + data['userId'] = user.get_id() + + return jsonify(**data) + + +@FLASK_CORE_APP.route('/api//', methods=['GET', 'POST', 'PUT', 'DELETE']) +def api_call(version, endpoint_path): + """Call an API endpoint directly over HTTP.""" + + # Get path and parameters + (path, path_parameters) = path_parser.parse(version, endpoint_path) + + query_parameters = request.args.to_dict() + for param in query_parameters: + try: + query_parameters[param] = int(query_parameters[param]) + except: + pass + + try: + body_parameters = json.loads(request.get_data()) + except: + body_parameters = {} + + # Create and call request + (req, response) = _process_message({ + 'id': 0, + 'method': request.method, + 'parameters': { + 'body': body_parameters, + 'path': path_parameters, + 'query': query_parameters + }, + 'path': path, + 'token': request.headers.get('auth-token') + }) + + print( + f'HTTP:\t{req.method} to `/{req.path}` resulted in {response.status["code"]}: {response.status["description"]}') + sys.stdout.flush() + + flask_response = jsonify(json.loads(response.to_JSON())) + flask_response.status_code = response.status['code'] + return flask_response + + +@FLASK_CORE_APP.route('/my-auth-token') +def serve_web_server_test(): + """Serve the web server test.""" + return send_from_directory(STATIC_ROOT, 'index.html') + + +@FLASK_CORE_APP.route('/') +@FLASK_CORE_APP.route('/projects') +@FLASK_CORE_APP.route('/projects/') +@FLASK_CORE_APP.route('/profile') +def serve_index(project_id=None): + return send_from_directory(STATIC_ROOT, 'index.html') + + +@SOCKET_IO_CORE.on('request') +def receive_message(message): + """"Receive a SocketIO request""" + (req, res) = _process_message(message) + + print(f'Socket: {req.method} to `/{req.path}` resulted in {res.status["code"]}: {res.status["description"]}') + sys.stdout.flush() + + flask_socketio.emit('response', res.to_JSON(), json=True) + + +def _process_message(message): + """Process a request message and return the response.""" + + try: + req = rest.Request(message) + res = req.process() + + return req, res + + except AuthorizationTokenError: + res = rest.Response(401, 'Authorization error') + res.id = message['id'] + + except RequestInitializationError as e: + res = rest.Response(400, str(e)) + res.id = message['id'] + + if not 'method' in message: + message['method'] = 'UNSPECIFIED' + if not 'path' in message: + message['path'] = 'UNSPECIFIED' + + except Exception: + res = rest.Response(500, 'Internal server error') + if 'id' in message: + res.id = message['id'] + traceback.print_exc() + + req = rest.Request() + req.method = message['method'] + req.path = message['path'] + + return req, res + + +if __name__ == '__main__': + print("Web server started on 8081") + SOCKET_IO_CORE.run(FLASK_CORE_APP, host='0.0.0.0', port=8081) diff --git a/api/misc/artwork/opendc-web-server-component-diagram.png b/api/misc/artwork/opendc-web-server-component-diagram.png new file mode 100644 index 00000000..91b26006 Binary files /dev/null and b/api/misc/artwork/opendc-web-server-component-diagram.png differ diff --git a/api/opendc/__init__.py b/api/opendc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/__init__.py b/api/opendc/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/__init__.py b/api/opendc/api/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/paths.json b/api/opendc/api/v2/paths.json new file mode 100644 index 00000000..90d5a2e6 --- /dev/null +++ b/api/opendc/api/v2/paths.json @@ -0,0 +1,18 @@ +[ + "/users", + "/users/{userId}", + "/projects", + "/projects/{projectId}", + "/projects/{projectId}/authorizations", + "/projects/{projectId}/topologies", + "/projects/{projectId}/portfolios", + "/topologies/{topologyId}", + "/portfolios/{portfolioId}", + "/portfolios/{portfolioId}/scenarios", + "/scenarios/{scenarioId}", + "/schedulers", + "/traces", + "/traces/{traceId}", + "/prefabs", + "/prefabs/{prefabId}" +] diff --git a/api/opendc/api/v2/portfolios/__init__.py b/api/opendc/api/v2/portfolios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/portfolios/portfolioId/__init__.py b/api/opendc/api/v2/portfolios/portfolioId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/portfolios/portfolioId/endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/endpoint.py new file mode 100644 index 00000000..c0ca64e0 --- /dev/null +++ b/api/opendc/api/v2/portfolios/portfolioId/endpoint.py @@ -0,0 +1,65 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def GET(request): + """Get this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved portfolio.', portfolio.obj) + + +def PUT(request): + """Update this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + }}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + portfolio.set_property('name', + request.params_body['portfolio']['name']) + portfolio.set_property('targets.enabledMetrics', + request.params_body['portfolio']['targets']['enabledMetrics']) + portfolio.set_property('targets.repeatsPerScenario', + request.params_body['portfolio']['targets']['repeatsPerScenario']) + + portfolio.update() + + return Response(200, 'Successfully updated portfolio.', portfolio.obj) + + +def DELETE(request): + """Delete this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + project = Project.from_id(portfolio.obj['projectId']) + project.check_exists() + if request.params_path['portfolioId'] in project.obj['portfolioIds']: + project.obj['portfolioIds'].remove(request.params_path['portfolioId']) + project.update() + + old_object = portfolio.delete() + + return Response(200, 'Successfully deleted portfolio.', old_object) diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py new file mode 100644 index 00000000..1c5e0ab6 --- /dev/null +++ b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py @@ -0,0 +1,43 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.scenario import Scenario +from opendc.util.rest import Response + + +def POST(request): + """Add a new Scenario for this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, + body={ + 'scenario': { + 'name': 'string', + 'trace': { + 'traceId': 'string', + 'loadSamplingFraction': 'float', + }, + 'topology': { + 'topologyId': 'string', + }, + 'operational': { + 'failuresEnabled': 'bool', + 'performanceInterferenceEnabled': 'bool', + 'schedulerName': 'string', + }, + } + }) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + scenario = Scenario(request.params_body['scenario']) + + scenario.set_property('portfolioId', request.params_path['portfolioId']) + scenario.set_property('simulationState', 'QUEUED') + + scenario.insert() + + portfolio.obj['scenarioIds'].append(scenario.get_id()) + portfolio.update() + + return Response(200, 'Successfully added Scenario.', scenario.obj) diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py new file mode 100644 index 00000000..8b55bab0 --- /dev/null +++ b/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py @@ -0,0 +1,119 @@ +from opendc.util.database import DB + + +def test_add_scenario_missing_parameter(client): + assert '400' in client.post('/api/v2/portfolios/1/scenarios').status + + +def test_add_scenario_non_existing_portfolio(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'portfolioIds': ['1'], + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }], + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + 'portfolioId': '1', + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }) + assert 'portfolioId' in res.json['content'] + assert 'simulationState' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py new file mode 100644 index 00000000..7ac346d4 --- /dev/null +++ b/api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py @@ -0,0 +1,149 @@ +from opendc.util.database import DB + + +def test_get_portfolio_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/portfolios/1').status + + +def test_get_portfolio_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) + res = client.get('/api/v2/portfolios/1') + assert '403' in res.status + + +def test_get_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'projectId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/portfolios/1') + assert '403' in res.status + + +def test_get_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'projectId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/portfolios/1') + assert '200' in res.status + + +def test_update_portfolio_missing_parameter(client): + assert '400' in client.put('/api/v2/portfolios/1').status + + +def test_update_portfolio_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/portfolios/1', json={'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/portfolios/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/portfolios/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'googleId': 'test', + 'portfolioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/portfolios/1') + assert '200' in res.status diff --git a/api/opendc/api/v2/prefabs/__init__.py b/api/opendc/api/v2/prefabs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/prefabs/endpoint.py b/api/opendc/api/v2/prefabs/endpoint.py new file mode 100644 index 00000000..723a2f0d --- /dev/null +++ b/api/opendc/api/v2/prefabs/endpoint.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from opendc.models.prefab import Prefab +from opendc.models.user import User +from opendc.util.database import Database +from opendc.util.rest import Response + + +def POST(request): + """Create a new prefab, and return that new prefab.""" + + request.check_required_parameters(body={'prefab': {'name': 'string'}}) + + prefab = Prefab(request.params_body['prefab']) + prefab.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) + prefab.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) + + user = User.from_google_id(request.google_id) + prefab.set_property('authorId', user.get_id()) + + prefab.insert() + + return Response(200, 'Successfully created prefab.', prefab.obj) diff --git a/api/opendc/api/v2/prefabs/prefabId/__init__.py b/api/opendc/api/v2/prefabs/prefabId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/prefabs/prefabId/endpoint.py b/api/opendc/api/v2/prefabs/prefabId/endpoint.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/prefabs/prefabId/test_endpoint.py b/api/opendc/api/v2/prefabs/prefabId/test_endpoint.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/prefabs/test_endpoint.py b/api/opendc/api/v2/prefabs/test_endpoint.py new file mode 100644 index 00000000..47029579 --- /dev/null +++ b/api/opendc/api/v2/prefabs/test_endpoint.py @@ -0,0 +1,22 @@ +from opendc.util.database import DB + + +def test_add_prefab_missing_parameter(client): + assert '400' in client.post('/api/v2/prefabs').status + + +def test_add_prefab(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'authorizations': []}) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': 1 + }) + res = client.post('/api/v2/prefabs', json={'prefab': {'name': 'test prefab'}}) + assert 'datetimeCreated' in res.json['content'] + assert 'datetimeLastEdited' in res.json['content'] + assert 'authorId' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/projects/__init__.py b/api/opendc/api/v2/projects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/projects/endpoint.py b/api/opendc/api/v2/projects/endpoint.py new file mode 100644 index 00000000..bf031382 --- /dev/null +++ b/api/opendc/api/v2/projects/endpoint.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from opendc.models.project import Project +from opendc.models.topology import Topology +from opendc.models.user import User +from opendc.util.database import Database +from opendc.util.rest import Response + + +def POST(request): + """Create a new project, and return that new project.""" + + request.check_required_parameters(body={'project': {'name': 'string'}}) + + topology = Topology({'name': 'Default topology', 'rooms': []}) + topology.insert() + + project = Project(request.params_body['project']) + project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) + project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) + project.set_property('topologyIds', [topology.get_id()]) + project.set_property('portfolioIds', []) + project.insert() + + topology.set_property('projectId', project.get_id()) + topology.update() + + user = User.from_google_id(request.google_id) + user.obj['authorizations'].append({'projectId': project.get_id(), 'authorizationLevel': 'OWN'}) + user.update() + + return Response(200, 'Successfully created project.', project.obj) diff --git a/api/opendc/api/v2/projects/projectId/__init__.py b/api/opendc/api/v2/projects/projectId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/projects/projectId/authorizations/__init__.py b/api/opendc/api/v2/projects/projectId/authorizations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/projects/projectId/authorizations/endpoint.py b/api/opendc/api/v2/projects/projectId/authorizations/endpoint.py new file mode 100644 index 00000000..9f6a60ec --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/authorizations/endpoint.py @@ -0,0 +1,17 @@ +from opendc.models.project import Project +from opendc.util.rest import Response + + +def GET(request): + """Find all authorizations for a Project.""" + + request.check_required_parameters(path={'projectId': 'string'}) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, False) + + authorizations = project.get_all_authorizations() + + return Response(200, 'Successfully retrieved project authorizations', authorizations) diff --git a/api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py b/api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py new file mode 100644 index 00000000..c3bbc093 --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py @@ -0,0 +1,40 @@ +from opendc.util.database import DB + + +def test_get_authorizations_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + mocker.patch.object(DB, 'fetch_all', return_value=None) + assert '404' in client.get('/api/v2/projects/1/authorizations').status + + +def test_get_authorizations_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'name': 'test trace', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'fetch_all', return_value=[]) + res = client.get('/api/v2/projects/1/authorizations') + assert '403' in res.status + + +def test_get_authorizations(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'name': 'test trace', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'fetch_all', return_value=[]) + res = client.get('/api/v2/projects/1/authorizations') + assert len(res.json['content']) == 0 + assert '200' in res.status diff --git a/api/opendc/api/v2/projects/projectId/endpoint.py b/api/opendc/api/v2/projects/projectId/endpoint.py new file mode 100644 index 00000000..77b66d75 --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/endpoint.py @@ -0,0 +1,66 @@ +from datetime import datetime + +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.models.topology import Topology +from opendc.models.user import User +from opendc.util.database import Database +from opendc.util.rest import Response + + +def GET(request): + """Get this Project.""" + + request.check_required_parameters(path={'projectId': 'string'}) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved project', project.obj) + + +def PUT(request): + """Update a project's name.""" + + request.check_required_parameters(body={'project': {'name': 'name'}}, path={'projectId': 'string'}) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + project.set_property('name', request.params_body['project']['name']) + project.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now())) + project.update() + + return Response(200, 'Successfully updated project.', project.obj) + + +def DELETE(request): + """Delete this Project.""" + + request.check_required_parameters(path={'projectId': 'string'}) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + for topology_id in project.obj['topologyIds']: + topology = Topology.from_id(topology_id) + topology.delete() + + for portfolio_id in project.obj['portfolioIds']: + portfolio = Portfolio.from_id(portfolio_id) + portfolio.delete() + + user = User.from_google_id(request.google_id) + user.obj['authorizations'] = list( + filter(lambda x: str(x['projectId']) != request.params_path['projectId'], user.obj['authorizations'])) + user.update() + + old_object = project.delete() + + return Response(200, 'Successfully deleted project.', old_object) diff --git a/api/opendc/api/v2/projects/projectId/portfolios/__init__.py b/api/opendc/api/v2/projects/projectId/portfolios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/api/opendc/api/v2/projects/projectId/portfolios/endpoint.py new file mode 100644 index 00000000..0bc65565 --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/portfolios/endpoint.py @@ -0,0 +1,35 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def POST(request): + """Add a new Portfolio for this Project.""" + + request.check_required_parameters(path={'projectId': 'string'}, + body={ + 'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + } + }) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + portfolio = Portfolio(request.params_body['portfolio']) + + portfolio.set_property('projectId', request.params_path['projectId']) + portfolio.set_property('scenarioIds', []) + + portfolio.insert() + + project.obj['portfolioIds'].append(portfolio.get_id()) + project.update() + + return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py new file mode 100644 index 00000000..24416cc3 --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py @@ -0,0 +1,83 @@ +from opendc.util.database import DB + + +def test_add_portfolio_missing_parameter(client): + assert '400' in client.post('/api/v2/projects/1/portfolios').status + + +def test_add_portfolio_non_existing_project(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.post('/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + }, + 'projectId': '1', + 'scenarioIds': [], + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }) + assert 'projectId' in res.json['content'] + assert 'scenarioIds' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/projects/projectId/test_endpoint.py b/api/opendc/api/v2/projects/projectId/test_endpoint.py new file mode 100644 index 00000000..7a862e8d --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/test_endpoint.py @@ -0,0 +1,119 @@ +from opendc.util.database import DB + + +def test_get_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/projects/1').status + + +def test_get_project_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'authorizations': []}) + res = client.get('/api/v2/projects/1') + assert '403' in res.status + + +def test_get_project_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/projects/1') + assert '403' in res.status + + +def test_get_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/projects/1') + assert '200' in res.status + + +def test_update_project_missing_parameter(client): + assert '400' in client.put('/api/v2/projects/1').status + + +def test_update_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}).status + + +def test_update_project_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}).status + + +def test_update_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/projects/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }], + 'topologyIds': [] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/projects/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'googleId': 'test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'topologyIds': [], + 'portfolioIds': [], + }) + mocker.patch.object(DB, 'update', return_value=None) + mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) + res = client.delete('/api/v2/projects/1') + assert '200' in res.status diff --git a/api/opendc/api/v2/projects/projectId/topologies/__init__.py b/api/opendc/api/v2/projects/projectId/topologies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/projects/projectId/topologies/endpoint.py b/api/opendc/api/v2/projects/projectId/topologies/endpoint.py new file mode 100644 index 00000000..211dc15d --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/topologies/endpoint.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from opendc.models.project import Project +from opendc.models.topology import Topology +from opendc.util.rest import Response +from opendc.util.database import Database + + +def POST(request): + """Add a new Topology to the specified project and return it""" + + request.check_required_parameters(path={'projectId': 'string'}, body={'topology': {'name': 'string'}}) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + topology = Topology({ + 'projectId': request.params_path['projectId'], + 'name': request.params_body['topology']['name'], + 'rooms': request.params_body['topology']['rooms'], + }) + + topology.insert() + + project.obj['topologyIds'].append(topology.get_id()) + project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) + project.update() + + return Response(200, 'Successfully inserted topology.', topology.obj) diff --git a/api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py b/api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py new file mode 100644 index 00000000..ca123a73 --- /dev/null +++ b/api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py @@ -0,0 +1,50 @@ +from opendc.util.database import DB + + +def test_add_topology_missing_parameter(client): + assert '400' in client.post('/api/v2/projects/1/topologies').status + + +def test_add_topology(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'topologyIds': [] + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'topologyIds': [] + }) + mocker.patch.object(DB, 'update', return_value={}) + res = client.post('/api/v2/projects/1/topologies', json={'topology': {'name': 'test project', 'rooms': []}}) + assert 'rooms' in res.json['content'] + assert '200' in res.status + + +def test_add_topology_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/projects/1/topologies', + json={ + 'topology': { + 'name': 'test_topology', + 'rooms': {} + } + }).status diff --git a/api/opendc/api/v2/projects/test_endpoint.py b/api/opendc/api/v2/projects/test_endpoint.py new file mode 100644 index 00000000..a50735b0 --- /dev/null +++ b/api/opendc/api/v2/projects/test_endpoint.py @@ -0,0 +1,23 @@ +from opendc.util.database import DB + + +def test_add_project_missing_parameter(client): + assert '400' in client.post('/api/v2/projects').status + + +def test_add_project(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'authorizations': []}) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'topologyIds': [] + }) + mocker.patch.object(DB, 'update', return_value={}) + res = client.post('/api/v2/projects', json={'project': {'name': 'test project'}}) + assert 'datetimeCreated' in res.json['content'] + assert 'datetimeLastEdited' in res.json['content'] + assert 'topologyIds' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/scenarios/__init__.py b/api/opendc/api/v2/scenarios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/scenarios/scenarioId/__init__.py b/api/opendc/api/v2/scenarios/scenarioId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/scenarios/scenarioId/endpoint.py b/api/opendc/api/v2/scenarios/scenarioId/endpoint.py new file mode 100644 index 00000000..02d39063 --- /dev/null +++ b/api/opendc/api/v2/scenarios/scenarioId/endpoint.py @@ -0,0 +1,57 @@ +from opendc.models.scenario import Scenario +from opendc.models.portfolio import Portfolio +from opendc.util.rest import Response + + +def GET(request): + """Get this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved scenario.', scenario.obj) + + +def PUT(request): + """Update this Scenarios name.""" + + request.check_required_parameters(path={'scenarioId': 'string'}, body={'scenario': { + 'name': 'string', + }}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + scenario.set_property('name', + request.params_body['scenario']['name']) + + scenario.update() + + return Response(200, 'Successfully updated scenario.', scenario.obj) + + +def DELETE(request): + """Delete this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + portfolio = Portfolio.from_id(scenario.obj['portfolioId']) + portfolio.check_exists() + if request.params_path['scenarioId'] in portfolio.obj['scenarioIds']: + portfolio.obj['scenarioIds'].remove(request.params_path['scenarioId']) + portfolio.update() + + old_object = scenario.delete() + + return Response(200, 'Successfully deleted scenario.', old_object) diff --git a/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py new file mode 100644 index 00000000..09b7d0c0 --- /dev/null +++ b/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py @@ -0,0 +1,140 @@ +from opendc.util.database import DB + + +def test_get_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/scenarios/1').status + + +def test_get_scenario_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={ + 'portfolioId': '1', + 'authorizations': [] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '200' in res.status + + +def test_update_scenario_missing_parameter(client): + assert '400' in client.put('/api/v2/scenarios/1').status + + +def test_update_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/scenarios/1', json={'scenario': { + 'name': 'test', + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/scenarios/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/scenarios/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'test', + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/scenarios/1') + assert '200' in res.status diff --git a/api/opendc/api/v2/schedulers/__init__.py b/api/opendc/api/v2/schedulers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/schedulers/endpoint.py b/api/opendc/api/v2/schedulers/endpoint.py new file mode 100644 index 00000000..a96fdd88 --- /dev/null +++ b/api/opendc/api/v2/schedulers/endpoint.py @@ -0,0 +1,9 @@ +from opendc.util.rest import Response + +SCHEDULERS = ['DEFAULT'] + + +def GET(_): + """Get all available Schedulers.""" + + return Response(200, 'Successfully retrieved Schedulers.', [{'name': name} for name in SCHEDULERS]) diff --git a/api/opendc/api/v2/schedulers/test_endpoint.py b/api/opendc/api/v2/schedulers/test_endpoint.py new file mode 100644 index 00000000..a0bd8758 --- /dev/null +++ b/api/opendc/api/v2/schedulers/test_endpoint.py @@ -0,0 +1,2 @@ +def test_get_schedulers(client): + assert '200' in client.get('/api/v2/schedulers').status diff --git a/api/opendc/api/v2/topologies/__init__.py b/api/opendc/api/v2/topologies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/topologies/topologyId/__init__.py b/api/opendc/api/v2/topologies/topologyId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/topologies/topologyId/endpoint.py b/api/opendc/api/v2/topologies/topologyId/endpoint.py new file mode 100644 index 00000000..512b050a --- /dev/null +++ b/api/opendc/api/v2/topologies/topologyId/endpoint.py @@ -0,0 +1,56 @@ +from datetime import datetime + +from opendc.util.database import Database +from opendc.models.project import Project +from opendc.models.topology import Topology +from opendc.util.rest import Response + + +def GET(request): + """Get this Topology.""" + + request.check_required_parameters(path={'topologyId': 'string'}) + + topology = Topology.from_id(request.params_path['topologyId']) + + topology.check_exists() + topology.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved topology.', topology.obj) + + +def PUT(request): + """Update this topology""" + request.check_required_parameters(path={'topologyId': 'string'}, body={'topology': {'name': 'string', 'rooms': {}}}) + topology = Topology.from_id(request.params_path['topologyId']) + + topology.check_exists() + topology.check_user_access(request.google_id, True) + + topology.set_property('name', request.params_body['topology']['name']) + topology.set_property('rooms', request.params_body['topology']['rooms']) + topology.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) + + topology.update() + + return Response(200, 'Successfully updated topology.', topology.obj) + + +def DELETE(request): + """Delete this topology""" + request.check_required_parameters(path={'topologyId': 'string'}) + + topology = Topology.from_id(request.params_path['topologyId']) + + topology.check_exists() + topology.check_user_access(request.google_id, True) + + project = Project.from_id(topology.obj['projectId']) + project.check_exists() + if request.params_path['topologyId'] in project.obj['topologyIds']: + project.obj['topologyIds'].remove(request.params_path['topologyId']) + project.update() + + old_object = topology.delete() + + return Response(200, 'Successfully deleted topology.', old_object) diff --git a/api/opendc/api/v2/topologies/topologyId/test_endpoint.py b/api/opendc/api/v2/topologies/topologyId/test_endpoint.py new file mode 100644 index 00000000..b25cb798 --- /dev/null +++ b/api/opendc/api/v2/topologies/topologyId/test_endpoint.py @@ -0,0 +1,116 @@ +from opendc.util.database import DB + + +def test_get_topology(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/topologies/1') + assert '200' in res.status + + +def test_get_topology_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/topologies/1').status + + +def test_get_topology_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/topologies/1') + assert '403' in res.status + + +def test_get_topology_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) + res = client.get('/api/v2/topologies/1') + assert '403' in res.status + + +def test_update_topology_missing_parameter(client): + assert '400' in client.put('/api/v2/topologies/1').status + + +def test_update_topology_non_existent(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/topologies/1', json={'topology': {'name': 'test_topology', 'rooms': {}}}).status + + +def test_update_topology_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/topologies/1', json={ + 'topology': { + 'name': 'updated_topology', + 'rooms': {} + } + }).status + + +def test_update_topology(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + + assert '200' in client.put('/api/v2/topologies/1', json={ + 'topology': { + 'name': 'updated_topology', + 'rooms': {} + } + }).status + + +def test_delete_topology(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'googleId': 'test', + 'topologyIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/topologies/1') + assert '200' in res.status + + +def test_delete_nonexistent_topology(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/topologies/1').status diff --git a/api/opendc/api/v2/traces/__init__.py b/api/opendc/api/v2/traces/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/traces/endpoint.py b/api/opendc/api/v2/traces/endpoint.py new file mode 100644 index 00000000..ee699e02 --- /dev/null +++ b/api/opendc/api/v2/traces/endpoint.py @@ -0,0 +1,10 @@ +from opendc.models.trace import Trace +from opendc.util.rest import Response + + +def GET(_): + """Get all available Traces.""" + + traces = Trace.get_all() + + return Response(200, 'Successfully retrieved Traces', traces.obj) diff --git a/api/opendc/api/v2/traces/test_endpoint.py b/api/opendc/api/v2/traces/test_endpoint.py new file mode 100644 index 00000000..9f806085 --- /dev/null +++ b/api/opendc/api/v2/traces/test_endpoint.py @@ -0,0 +1,6 @@ +from opendc.util.database import DB + + +def test_get_traces(client, mocker): + mocker.patch.object(DB, 'fetch_all', return_value=[]) + assert '200' in client.get('/api/v2/traces').status diff --git a/api/opendc/api/v2/traces/traceId/__init__.py b/api/opendc/api/v2/traces/traceId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/traces/traceId/endpoint.py b/api/opendc/api/v2/traces/traceId/endpoint.py new file mode 100644 index 00000000..670f88d1 --- /dev/null +++ b/api/opendc/api/v2/traces/traceId/endpoint.py @@ -0,0 +1,14 @@ +from opendc.models.trace import Trace +from opendc.util.rest import Response + + +def GET(request): + """Get this Trace.""" + + request.check_required_parameters(path={'traceId': 'string'}) + + trace = Trace.from_id(request.params_path['traceId']) + + trace.check_exists() + + return Response(200, 'Successfully retrieved trace.', trace.obj) diff --git a/api/opendc/api/v2/traces/traceId/test_endpoint.py b/api/opendc/api/v2/traces/traceId/test_endpoint.py new file mode 100644 index 00000000..56792ca9 --- /dev/null +++ b/api/opendc/api/v2/traces/traceId/test_endpoint.py @@ -0,0 +1,13 @@ +from opendc.util.database import DB + + +def test_get_trace_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/traces/1').status + + +def test_get_trace(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'name': 'test trace'}) + res = client.get('/api/v2/traces/1') + assert 'name' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/users/__init__.py b/api/opendc/api/v2/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/users/endpoint.py b/api/opendc/api/v2/users/endpoint.py new file mode 100644 index 00000000..0dcf2463 --- /dev/null +++ b/api/opendc/api/v2/users/endpoint.py @@ -0,0 +1,30 @@ +from opendc.models.user import User +from opendc.util.rest import Response + + +def GET(request): + """Search for a User using their email address.""" + + request.check_required_parameters(query={'email': 'string'}) + + user = User.from_email(request.params_query['email']) + + user.check_exists() + + return Response(200, 'Successfully retrieved user.', user.obj) + + +def POST(request): + """Add a new User.""" + + request.check_required_parameters(body={'user': {'email': 'string'}}) + + user = User(request.params_body['user']) + user.set_property('googleId', request.google_id) + user.set_property('authorizations', []) + + user.check_already_exists() + + user.insert() + + return Response(200, 'Successfully created user.', user.obj) diff --git a/api/opendc/api/v2/users/test_endpoint.py b/api/opendc/api/v2/users/test_endpoint.py new file mode 100644 index 00000000..d60429b3 --- /dev/null +++ b/api/opendc/api/v2/users/test_endpoint.py @@ -0,0 +1,34 @@ +from opendc.util.database import DB + + +def test_get_user_by_email_missing_parameter(client): + assert '400' in client.get('/api/v2/users').status + + +def test_get_user_by_email_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/users?email=test@test.com').status + + +def test_get_user_by_email(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) + res = client.get('/api/v2/users?email=test@test.com') + assert 'email' in res.json['content'] + assert '200' in res.status + + +def test_add_user_missing_parameter(client): + assert '400' in client.post('/api/v2/users').status + + +def test_add_user_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) + assert '409' in client.post('/api/v2/users', json={'user': {'email': 'test@test.com'}}).status + + +def test_add_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + mocker.patch.object(DB, 'insert', return_value={'email': 'test@test.com'}) + res = client.post('/api/v2/users', json={'user': {'email': 'test@test.com'}}) + assert 'email' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/api/v2/users/userId/__init__.py b/api/opendc/api/v2/users/userId/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/api/v2/users/userId/endpoint.py b/api/opendc/api/v2/users/userId/endpoint.py new file mode 100644 index 00000000..be3462c0 --- /dev/null +++ b/api/opendc/api/v2/users/userId/endpoint.py @@ -0,0 +1,59 @@ +from opendc.models.project import Project +from opendc.models.user import User +from opendc.util.rest import Response + + +def GET(request): + """Get this User.""" + + request.check_required_parameters(path={'userId': 'string'}) + + user = User.from_id(request.params_path['userId']) + + user.check_exists() + + return Response(200, 'Successfully retrieved user.', user.obj) + + +def PUT(request): + """Update this User's given name and/or family name.""" + + request.check_required_parameters(body={'user': { + 'givenName': 'string', + 'familyName': 'string' + }}, + path={'userId': 'string'}) + + user = User.from_id(request.params_path['userId']) + + user.check_exists() + user.check_correct_user(request.google_id) + + user.set_property('givenName', request.params_body['user']['givenName']) + user.set_property('familyName', request.params_body['user']['familyName']) + + user.update() + + return Response(200, 'Successfully updated user.', user.obj) + + +def DELETE(request): + """Delete this User.""" + + request.check_required_parameters(path={'userId': 'string'}) + + user = User.from_id(request.params_path['userId']) + + user.check_exists() + user.check_correct_user(request.google_id) + + for authorization in user.obj['authorizations']: + if authorization['authorizationLevel'] != 'OWN': + continue + + project = Project.from_id(authorization['projectId']) + project.delete() + + old_object = user.delete() + + return Response(200, 'Successfully deleted user.', old_object) diff --git a/api/opendc/api/v2/users/userId/test_endpoint.py b/api/opendc/api/v2/users/userId/test_endpoint.py new file mode 100644 index 00000000..cdff2229 --- /dev/null +++ b/api/opendc/api/v2/users/userId/test_endpoint.py @@ -0,0 +1,53 @@ +from opendc.util.database import DB + + +def test_get_user_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/users/1').status + + +def test_get_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) + res = client.get('/api/v2/users/1') + assert 'email' in res.json['content'] + assert '200' in res.status + + +def test_update_user_missing_parameter(client): + assert '400' in client.put('/api/v2/users/1').status + + +def test_update_user_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status + + +def test_update_user_different_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'other_test'}) + assert '403' in client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status + + +def test_update_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'test'}) + mocker.patch.object(DB, 'update', return_value={'givenName': 'A', 'familyName': 'B'}) + res = client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}) + assert 'givenName' in res.json['content'] + assert '200' in res.status + + +def test_delete_user_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.delete('/api/v2/users/1').status + + +def test_delete_user_different_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'other_test'}) + assert '403' in client.delete('/api/v2/users/1').status + + +def test_delete_user(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'test', 'authorizations': []}) + mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) + res = client.delete('/api/v2/users/1') + assert 'googleId' in res.json['content'] + assert '200' in res.status diff --git a/api/opendc/models/__init__.py b/api/opendc/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/models/model.py b/api/opendc/models/model.py new file mode 100644 index 00000000..bcb833ae --- /dev/null +++ b/api/opendc/models/model.py @@ -0,0 +1,59 @@ +from uuid import uuid4 + +from opendc.util.database import DB +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Model: + """Base class for all models.""" + + collection_name = '' + + @classmethod + def from_id(cls, _id): + """Fetches the document with given ID from the collection.""" + return cls(DB.fetch_one({'_id': _id}, cls.collection_name)) + + @classmethod + def get_all(cls): + """Fetches all documents from the collection.""" + return cls(DB.fetch_all({}, cls.collection_name)) + + def __init__(self, obj): + self.obj = obj + + def get_id(self): + """Returns the ID of the enclosed object.""" + return str(self.obj['_id']) + + def check_exists(self): + """Raises an error if the enclosed object does not exist.""" + if self.obj is None: + raise ClientError(Response(404, 'Not found.')) + + def set_property(self, key, value): + """Sets the given property on the enclosed object, with support for simple nested access.""" + if '.' in key: + keys = key.split('.') + self.obj[keys[0]][keys[1]] = value + else: + self.obj[key] = value + + def insert(self): + """Inserts the enclosed object and generates a UUID for it.""" + self.obj['_id'] = str(uuid4()) + DB.insert(self.obj, self.collection_name) + + def update(self): + """Updates the enclosed object and updates the internal reference to the newly inserted object.""" + DB.update(self.get_id(), self.obj, self.collection_name) + + def delete(self): + """Deletes the enclosed object in the database, if it existed.""" + if self.obj is None: + return None + + old_object = self.obj.copy() + DB.delete_one({'_id': self.get_id()}, self.collection_name) + return old_object diff --git a/api/opendc/models/portfolio.py b/api/opendc/models/portfolio.py new file mode 100644 index 00000000..32961b63 --- /dev/null +++ b/api/opendc/models/portfolio.py @@ -0,0 +1,24 @@ +from opendc.models.model import Model +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Portfolio(Model): + """Model representing a Portfolio.""" + + collection_name = 'portfolios' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + user = User.from_google_id(google_id) + authorizations = list( + filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving/editing portfolio.')) diff --git a/api/opendc/models/prefab.py b/api/opendc/models/prefab.py new file mode 100644 index 00000000..70910c4a --- /dev/null +++ b/api/opendc/models/prefab.py @@ -0,0 +1,26 @@ +from opendc.models.model import Model +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Prefab(Model): + """Model representing a Project.""" + + collection_name = 'prefabs' + + def check_user_access(self, google_id): + """Raises an error if the user with given [google_id] has insufficient access to view this prefab. + + :param google_id: The Google ID of the user. + """ + user = User.from_google_id(google_id) + + #try: + + print(self.obj) + if self.obj['authorId'] != user.get_id() and self.obj['visibility'] == "private": + raise ClientError(Response(403, "Forbidden from retrieving prefab.")) + #except KeyError: + # OpenDC-authored objects don't necessarily have an authorId + # return diff --git a/api/opendc/models/project.py b/api/opendc/models/project.py new file mode 100644 index 00000000..b57e9f77 --- /dev/null +++ b/api/opendc/models/project.py @@ -0,0 +1,31 @@ +from opendc.models.model import Model +from opendc.models.user import User +from opendc.util.database import DB +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Project(Model): + """Model representing a Project.""" + + collection_name = 'projects' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + user = User.from_google_id(google_id) + authorizations = list(filter(lambda x: str(x['projectId']) == str(self.get_id()), + user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, "Forbidden from retrieving project.")) + + def get_all_authorizations(self): + """Get all user IDs having access to this project.""" + return [ + str(user['_id']) for user in DB.fetch_all({'authorizations': { + 'projectId': self.obj['_id'] + }}, User.collection_name) + ] diff --git a/api/opendc/models/scenario.py b/api/opendc/models/scenario.py new file mode 100644 index 00000000..8d53e408 --- /dev/null +++ b/api/opendc/models/scenario.py @@ -0,0 +1,26 @@ +from opendc.models.model import Model +from opendc.models.portfolio import Portfolio +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Scenario(Model): + """Model representing a Scenario.""" + + collection_name = 'scenarios' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + portfolio = Portfolio.from_id(self.obj['portfolioId']) + user = User.from_google_id(google_id) + authorizations = list( + filter(lambda x: str(x['projectId']) == str(portfolio.obj['projectId']), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) diff --git a/api/opendc/models/topology.py b/api/opendc/models/topology.py new file mode 100644 index 00000000..cb4c4bab --- /dev/null +++ b/api/opendc/models/topology.py @@ -0,0 +1,27 @@ +from opendc.models.model import Model +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Topology(Model): + """Model representing a Project.""" + + collection_name = 'topologies' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + user = User.from_google_id(google_id) + if 'projectId' not in self.obj: + raise ClientError(Response(400, 'Missing projectId in topology.')) + + authorizations = list( + filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving topology.')) diff --git a/api/opendc/models/trace.py b/api/opendc/models/trace.py new file mode 100644 index 00000000..2f6e4926 --- /dev/null +++ b/api/opendc/models/trace.py @@ -0,0 +1,7 @@ +from opendc.models.model import Model + + +class Trace(Model): + """Model representing a Trace.""" + + collection_name = 'traces' diff --git a/api/opendc/models/user.py b/api/opendc/models/user.py new file mode 100644 index 00000000..8e8ff945 --- /dev/null +++ b/api/opendc/models/user.py @@ -0,0 +1,36 @@ +from opendc.models.model import Model +from opendc.util.database import DB +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class User(Model): + """Model representing a User.""" + + collection_name = 'users' + + @classmethod + def from_email(cls, email): + """Fetches the user with given email from the collection.""" + return User(DB.fetch_one({'email': email}, User.collection_name)) + + @classmethod + def from_google_id(cls, google_id): + """Fetches the user with given Google ID from the collection.""" + return User(DB.fetch_one({'googleId': google_id}, User.collection_name)) + + def check_correct_user(self, request_google_id): + """Raises an error if a user tries to modify another user. + + :param request_google_id: + """ + if request_google_id is not None and self.obj['googleId'] != request_google_id: + raise ClientError(Response(403, f'Forbidden from editing user with ID {self.obj["_id"]}.')) + + def check_already_exists(self): + """Checks if the user already exists in the database.""" + + existing_user = DB.fetch_one({'googleId': self.obj['googleId']}, self.collection_name) + + if existing_user is not None: + raise ClientError(Response(409, 'User already exists.')) diff --git a/api/opendc/util/__init__.py b/api/opendc/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/opendc/util/database.py b/api/opendc/util/database.py new file mode 100644 index 00000000..80cdcbab --- /dev/null +++ b/api/opendc/util/database.py @@ -0,0 +1,92 @@ +import json +import urllib.parse +from datetime import datetime + +from bson.json_util import dumps +from pymongo import MongoClient + +DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S' +CONNECTION_POOL = None + + +class Database: + """Object holding functionality for database access.""" + def __init__(self): + self.opendc_db = None + + def initialize_database(self, user, password, database, host): + """Initializes the database connection.""" + + user = urllib.parse.quote_plus(user) + password = urllib.parse.quote_plus(password) + database = urllib.parse.quote_plus(database) + host = urllib.parse.quote_plus(host) + + client = MongoClient('mongodb://%s:%s@%s/default_db?authSource=%s' % (user, password, host, database)) + self.opendc_db = client.opendc + + def fetch_one(self, query, collection): + """Uses existing mongo connection to return a single (the first) document in a collection matching the given + query as a JSON object. + + The query needs to be in json format, i.e.: `{'name': prefab_name}`. + """ + bson = getattr(self.opendc_db, collection).find_one(query) + + return self.convert_bson_to_json(bson) + + def fetch_all(self, query, collection): + """Uses existing mongo connection to return all documents matching a given query, as a list of JSON objects. + + The query needs to be in json format, i.e.: `{'name': prefab_name}`. + """ + results = [] + cursor = getattr(self.opendc_db, collection).find(query) + for doc in cursor: + results.append(self.convert_bson_to_json(doc)) + return results + + def insert(self, obj, collection): + """Updates an existing object.""" + bson = getattr(self.opendc_db, collection).insert(obj) + + return self.convert_bson_to_json(bson) + + def update(self, _id, obj, collection): + """Updates an existing object.""" + bson = getattr(self.opendc_db, collection).update({'_id': _id}, obj) + + return self.convert_bson_to_json(bson) + + def delete_one(self, query, collection): + """Deletes one object matching the given query. + + The query needs to be in json format, i.e.: `{'name': prefab_name}`. + """ + getattr(self.opendc_db, collection).delete_one(query) + + def delete_all(self, query, collection): + """Deletes all objects matching the given query. + + The query needs to be in json format, i.e.: `{'name': prefab_name}`. + """ + getattr(self.opendc_db, collection).delete_many(query) + + @staticmethod + def convert_bson_to_json(bson): + """Converts a BSON representation to JSON and returns the JSON representation.""" + json_string = dumps(bson) + return json.loads(json_string) + + @staticmethod + def datetime_to_string(datetime_to_convert): + """Return a database-compatible string representation of the given datetime object.""" + return datetime_to_convert.strftime(DATETIME_STRING_FORMAT) + + @staticmethod + def string_to_datetime(string_to_convert): + """Return a datetime corresponding to the given string representation.""" + return datetime.strptime(string_to_convert, DATETIME_STRING_FORMAT) + + +DB = Database() diff --git a/api/opendc/util/exceptions.py b/api/opendc/util/exceptions.py new file mode 100644 index 00000000..7724a407 --- /dev/null +++ b/api/opendc/util/exceptions.py @@ -0,0 +1,64 @@ +class RequestInitializationError(Exception): + """Raised when a Request cannot successfully be initialized""" + + +class UnimplementedEndpointError(RequestInitializationError): + """Raised when a Request path does not point to a module.""" + + +class MissingRequestParameterError(RequestInitializationError): + """Raised when a Request does not contain one or more required parameters.""" + + +class UnsupportedMethodError(RequestInitializationError): + """Raised when a Request does not use a supported REST method. + + The method must be in all-caps, supported by REST, and implemented by the module. + """ + + +class AuthorizationTokenError(RequestInitializationError): + """Raised when an authorization token is not correctly verified.""" + + +class ForeignKeyError(Exception): + """Raised when a foreign key constraint is not met.""" + + +class RowNotFoundError(Exception): + """Raised when a database row is not found.""" + def __init__(self, table_name): + super(RowNotFoundError, self).__init__('Row in `{}` table not found.'.format(table_name)) + + self.table_name = table_name + + +class ParameterError(Exception): + """Raised when a parameter is either missing or incorrectly typed.""" + + +class IncorrectParameterError(ParameterError): + """Raised when a parameter is of the wrong type.""" + def __init__(self, parameter_name, parameter_location): + super(IncorrectParameterError, + self).__init__('Incorrectly typed `{}` {} parameter.'.format(parameter_name, parameter_location)) + + self.parameter_name = parameter_name + self.parameter_location = parameter_location + + +class MissingParameterError(ParameterError): + """Raised when a parameter is missing.""" + def __init__(self, parameter_name, parameter_location): + super(MissingParameterError, + self).__init__('Missing required `{}` {} parameter.'.format(parameter_name, parameter_location)) + + self.parameter_name = parameter_name + self.parameter_location = parameter_location + + +class ClientError(Exception): + """Raised when a 4xx response is to be returned.""" + def __init__(self, response): + super(ClientError, self).__init__(str(response)) + self.response = response diff --git a/api/opendc/util/parameter_checker.py b/api/opendc/util/parameter_checker.py new file mode 100644 index 00000000..14dd1dc0 --- /dev/null +++ b/api/opendc/util/parameter_checker.py @@ -0,0 +1,85 @@ +from opendc.util import exceptions +from opendc.util.database import Database + + +def _missing_parameter(params_required, params_actual, parent=''): + """Recursively search for the first missing parameter.""" + + for param_name in params_required: + + if param_name not in params_actual: + return '{}.{}'.format(parent, param_name) + + param_required = params_required.get(param_name) + param_actual = params_actual.get(param_name) + + if isinstance(param_required, dict): + + param_missing = _missing_parameter(param_required, param_actual, param_name) + + if param_missing is not None: + return '{}.{}'.format(parent, param_missing) + + return None + + +def _incorrect_parameter(params_required, params_actual, parent=''): + """Recursively make sure each parameter is of the correct type.""" + + for param_name in params_required: + + param_required = params_required.get(param_name) + param_actual = params_actual.get(param_name) + + if isinstance(param_required, dict): + + param_incorrect = _incorrect_parameter(param_required, param_actual, param_name) + + if param_incorrect is not None: + return '{}.{}'.format(parent, param_incorrect) + + else: + + if param_required == 'datetime': + try: + Database.string_to_datetime(param_actual) + except: + return '{}.{}'.format(parent, param_name) + + type_pairs = [ + ('int', (int,)), + ('float', (float, int)), + ('bool', (bool,)), + ('string', (str, int)), + ('list', (list,)), + ] + + for str_type, actual_types in type_pairs: + if param_required == str_type and all(not isinstance(param_actual, t) + for t in actual_types): + return '{}.{}'.format(parent, param_name) + + return None + + +def _format_parameter(parameter): + """Format the output of a parameter check.""" + + parts = parameter.split('.') + inner = ['["{}"]'.format(x) for x in parts[2:]] + return parts[1] + ''.join(inner) + + +def check(request, **kwargs): + """Check if all required parameters are there.""" + + for location, params_required in kwargs.items(): + params_actual = getattr(request, 'params_{}'.format(location)) + + missing_parameter = _missing_parameter(params_required, params_actual) + if missing_parameter is not None: + raise exceptions.MissingParameterError(_format_parameter(missing_parameter), location) + + incorrect_parameter = _incorrect_parameter(params_required, params_actual) + if incorrect_parameter is not None: + raise exceptions.IncorrectParameterError(_format_parameter(incorrect_parameter), location) diff --git a/api/opendc/util/path_parser.py b/api/opendc/util/path_parser.py new file mode 100644 index 00000000..a8bbdeba --- /dev/null +++ b/api/opendc/util/path_parser.py @@ -0,0 +1,39 @@ +import json +import os + + +def parse(version, endpoint_path): + """Map an HTTP endpoint path to an API path""" + + # Get possible paths + with open(os.path.join(os.path.dirname(__file__), '..', 'api', '{}', 'paths.json').format(version)) as paths_file: + paths = json.load(paths_file) + + # Find API path that matches endpoint_path + endpoint_path_parts = endpoint_path.strip('/').split('/') + paths_parts = [x.strip('/').split('/') for x in paths if len(x.strip('/').split('/')) == len(endpoint_path_parts)] + path = None + + for path_parts in paths_parts: + found = True + for (endpoint_part, part) in zip(endpoint_path_parts, path_parts): + if not part.startswith('{') and endpoint_part != part: + found = False + break + if found: + path = path_parts + + if path is None: + return None + + # Extract path parameters + parameters = {} + + for (name, value) in zip(path, endpoint_path_parts): + if name.startswith('{'): + try: + parameters[name.strip('{}')] = int(value) + except: + parameters[name.strip('{}')] = value + + return '{}/{}'.format(version, '/'.join(path)), parameters diff --git a/api/opendc/util/rest.py b/api/opendc/util/rest.py new file mode 100644 index 00000000..abd2f3de --- /dev/null +++ b/api/opendc/util/rest.py @@ -0,0 +1,141 @@ +import importlib +import json +import os + +from oauth2client import client, crypt + +from opendc.util import exceptions, parameter_checker +from opendc.util.exceptions import ClientError + + +class Request: + """WebSocket message to REST request mapping.""" + def __init__(self, message=None): + """"Initialize a Request from a socket message.""" + + # Get the Request parameters from the message + + if message is None: + return + + try: + self.message = message + + self.id = message['id'] + + self.path = message['path'] + self.method = message['method'] + + self.params_body = message['parameters']['body'] + self.params_path = message['parameters']['path'] + self.params_query = message['parameters']['query'] + + self.token = message['token'] + + except KeyError as exception: + raise exceptions.MissingRequestParameterError(exception) + + # Parse the path and import the appropriate module + + try: + self.path = message['path'].strip('/') + + module_base = 'opendc.api.{}.endpoint' + module_path = self.path.replace('{', '').replace('}', '').replace('/', '.') + + self.module = importlib.import_module(module_base.format(module_path)) + except ImportError as e: + print(e) + raise exceptions.UnimplementedEndpointError('Unimplemented endpoint: {}.'.format(self.path)) + + # Check the method + + if self.method not in ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']: + raise exceptions.UnsupportedMethodError('Non-rest method: {}'.format(self.method)) + + if not hasattr(self.module, self.method): + raise exceptions.UnsupportedMethodError('Unimplemented method at endpoint {}: {}'.format( + self.path, self.method)) + + # Verify the user + + if "OPENDC_FLASK_TESTING" in os.environ: + self.google_id = 'test' + return + + try: + self.google_id = self._verify_token(self.token) + except crypt.AppIdentityError as e: + raise exceptions.AuthorizationTokenError(e) + + def check_required_parameters(self, **kwargs): + """Raise an error if a parameter is missing or of the wrong type.""" + + try: + parameter_checker.check(self, **kwargs) + except exceptions.ParameterError as e: + raise ClientError(Response(400, str(e))) + + def process(self): + """Process the Request and return a Response.""" + + method = getattr(self.module, self.method) + + try: + response = method(self) + except ClientError as e: + e.response.id = self.id + return e.response + + response.id = self.id + + return response + + def to_JSON(self): + """Return a JSON representation of this Request""" + + self.message['id'] = 0 + self.message['token'] = None + + return json.dumps(self.message) + + @staticmethod + def _verify_token(token): + """Return the ID of the signed-in user. + + Or throw an Exception if the token is invalid. + """ + + try: + id_info = client.verify_id_token(token, os.environ['OPENDC_OAUTH_CLIENT_ID']) + except Exception as e: + print(e) + raise crypt.AppIdentityError('Exception caught trying to verify ID token: {}'.format(e)) + + if id_info['aud'] != os.environ['OPENDC_OAUTH_CLIENT_ID']: + raise crypt.AppIdentityError('Unrecognized client.') + + if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: + raise crypt.AppIdentityError('Wrong issuer.') + + return id_info['sub'] + + +class Response: + """Response to websocket mapping""" + def __init__(self, status_code, status_description, content=None): + """Initialize a new Response.""" + + self.id = 0 + self.status = {'code': status_code, 'description': status_description} + self.content = content + + def to_JSON(self): + """"Return a JSON representation of this Response""" + + data = {'id': self.id, 'status': self.status} + + if self.content is not None: + data['content'] = self.content + + return json.dumps(data) diff --git a/api/pytest.ini b/api/pytest.ini new file mode 100644 index 00000000..775a8ff4 --- /dev/null +++ b/api/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +env = + OPENDC_FLASK_TESTING=True + OPENDC_FLASK_SECRET=Secret + OPENDC_SERVER_BASE_URL=localhost diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 00000000..140a046f --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,15 @@ +flask==1.1.2 +flask-socketio==4.3.0 +oauth2client==4.1.3 +eventlet==0.25.2 +flask-compress==1.5.0 +flask-cors==3.0.8 +pyasn1-modules==0.2.8 +six==1.15.0 +pymongo==3.10.1 +yapf==0.30.0 +pytest==5.4.3 +pytest-mock==3.1.1 +pytest-env==0.6.2 +pylint==2.5.3 +python-dotenv==0.13.0 diff --git a/api/static/index.html b/api/static/index.html new file mode 100644 index 00000000..ac78cbfb --- /dev/null +++ b/api/static/index.html @@ -0,0 +1,22 @@ + + + + + + +
+Sign out + +

Your auth token:

+

Loading...

\ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b2072be9..6dc01f67 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3" services: - frontend: - build: ./ - image: frontend + api: + build: ./api + image: api restart: on-failure ports: - "8081:8081" diff --git a/web-server/.gitignore b/web-server/.gitignore deleted file mode 100644 index fef0da65..00000000 --- a/web-server/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -.DS_Store -*.pyc -*.pyo -venv -venv* -dist -build -*.egg -*.egg-info -_mailinglist -.tox -.cache/ -.idea/ -config.json -test.json diff --git a/web-server/.gitlab-ci.yml b/web-server/.gitlab-ci.yml deleted file mode 100644 index d80ba836..00000000 --- a/web-server/.gitlab-ci.yml +++ /dev/null @@ -1,26 +0,0 @@ -image: "python:3.8" - -variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - -cache: - paths: - - .cache/pip - -stages: - - static-analysis - - test - -static-analysis: - stage: static-analysis - script: - - python --version - - pip install -r requirements.txt - - pylint opendc - -test: - stage: test - script: - - python --version - - pip install -r requirements.txt - - pytest opendc diff --git a/web-server/.pylintrc b/web-server/.pylintrc deleted file mode 100644 index f25e4fc2..00000000 --- a/web-server/.pylintrc +++ /dev/null @@ -1,521 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=duplicate-code, - missing-module-docstring, - invalid-name, - bare-except, - too-few-public-methods - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=12 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/web-server/.style.yapf b/web-server/.style.yapf deleted file mode 100644 index f5c26c57..00000000 --- a/web-server/.style.yapf +++ /dev/null @@ -1,3 +0,0 @@ -[style] -based_on_style = pep8 -column_limit=120 diff --git a/web-server/README.md b/web-server/README.md deleted file mode 100644 index 84fd09cc..00000000 --- a/web-server/README.md +++ /dev/null @@ -1,101 +0,0 @@ -

- OpenDC -
- OpenDC Web Server -

-

- Collaborative Datacenter Simulation and Exploration for Everybody -

- -
- -The OpenDC web server is the bridge between OpenDC's frontend and database. It is built with Flask/SocketIO in Python and implements the OpenAPI-compliant [OpenDC API specification](../opendc-api-spec.yml). - -This document explains a high-level view of the web server architecture ([jump](#architecture)), and describes how to set up the web server for local development ([jump](#setup-for-local-development)). - -## Architecture - -The following diagram shows a high-level view of the architecture of the OpenDC web server. Squared-off colored boxes indicate packages (colors become more saturated as packages are nested); rounded-off boxes indicate individual components; dotted lines indicate control flow; and solid lines indicate data flow. - -![OpenDC Web Server Component Diagram](misc/artwork/opendc-web-server-component-diagram.png) - -The OpenDC API is implemented by the `Main Server Loop`, which is the only component in the base package. - -### Util Package - -The `Util` package handles several miscellaneous tasks: - -* `Database API`: Wraps database access functionality used by `Models` to read themselves from/write themselves into the database. -* `Exceptions`: Holds definitions for exceptions used throughout the web server. -* `Parameter Checker`: Recursively checks whether required `Request` parameters are present and correctly typed. -* `REST`: Parses SocketIO and HTTP messages into `Request` objects, and calls the appropriate `API` endpoint to get a `Response` object to return to the `Main Server Loop`. - -### API Package - -The `API` package contains the logic for the HTTP methods in each API endpoint. Packages are structured to mirror the API: the code for the endpoint `GET api/projects`, for example, would be located at the `endpoint.py` inside the `projects` package (so at `api/projects/endpoint.py`). - -An `endpoint.py` file contains methods for each HTTP method it supports, which takes a request as input (such as `def GET(request):`). Typically, such a method checks whether the parameters were passed correctly (using the `Parameter Checker`); fetches some model from the database; checks whether the data exists and is accessible by the user who made the request; possibly modifies this data and writes it back to the database; and returns a JSON representation of the model. - -The `REST` component dynamically imports the appropriate method from the appropriate `endpoint`, according to request it receives, and executes it. - -### Models Package - -The `models` package contains the logic for mapping Python objects to their database representations. This involves an abstract `model` which has generic CRUD operations. Extensions of `model`, such as a `User` or `Project`, specify some more specific operations and their collection metadata. - -`Endpoint`s import these `models` and use them to execute requests. - -## Setup for Local Development - -The following steps will guide you through setting up the OpenDC web server locally for development. To test individual endpoints, edit `static/index.html`. - -### Local Setup - -#### Install requirements - -Make sure you have Python 3.7+ installed (if not, get it [here](https://www.python.org/)), as well as pip (if not, get it [here](https://pip.pypa.io/en/stable/installing/)). Then run the following to install the requirements. - -```bash -pip install -r requirements.txt -``` - -The web server also requires a running MongoDB instance. We recommend setting this up through docker, by running `docker-compose build` and `docker-compose up` in the [`mongodb` directory](../database) of the main OpenDC repository. - -#### Get and configure the code - -Clone OpenDC and follow the [instructions in the main repository](../) to set up a Google OAuth ID and environment variables. - -**Important:** Be sure to set up environment variables according to those instructions, in a `.env` file. - -In `opendc-web-server/static/index.html`, add your own `OAUTH_CLIENT_ID` in `content=` on line `2`. - -#### Set up the database - -You can selectively run only the database services from the standard OpenDC `docker-compose` setup: - -```bash -docker-compose build mongo mongo-express -docker-compose up mongo mongo-express -``` - -This will set you up with a running MongoDB instance and a visual inspection tool running on [localhost:8082](http://localhost:8082), with which you can view and manipulate the database. - -### Local Development - -Run the server. - -```bash -cd web-server -python main.py -``` - -When editing the web server code, restart the server (`CTRL` + `c` followed by `python main.py` in the console running the server) to see the result of your changes. - -#### Code Style - -To format all files, run `format.sh` in this directory (uses `yapf` internally). - -To check if code style is up to modern standards, run `check.sh` in this directory (uses `pylint` internally). - -#### Testing - -Run `pytest` in this directory to run all tests. diff --git a/web-server/check.sh b/web-server/check.sh deleted file mode 100755 index abe2c596..00000000 --- a/web-server/check.sh +++ /dev/null @@ -1 +0,0 @@ -pylint opendc --ignore-patterns=test_.*?py diff --git a/web-server/conftest.py b/web-server/conftest.py deleted file mode 100644 index 1f4831b8..00000000 --- a/web-server/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Configuration file for all unit tests. -""" -import pytest - -from main import FLASK_CORE_APP - - -@pytest.fixture -def client(): - """Returns a Flask API client to interact with.""" - FLASK_CORE_APP.config['TESTING'] = True - - with FLASK_CORE_APP.test_client() as client: - yield client diff --git a/web-server/format.sh b/web-server/format.sh deleted file mode 100755 index 18cba452..00000000 --- a/web-server/format.sh +++ /dev/null @@ -1 +0,0 @@ -yapf **/*.py -i diff --git a/web-server/main.py b/web-server/main.py deleted file mode 100644 index c466c0f2..00000000 --- a/web-server/main.py +++ /dev/null @@ -1,196 +0,0 @@ -import flask_socketio -import json -import os -import sys -import traceback -import urllib.request -from flask import Flask, request, send_from_directory, jsonify -from flask_compress import Compress -from oauth2client import client, crypt -from flask_cors import CORS -from dotenv import load_dotenv - -from opendc.models.user import User -from opendc.util import rest, path_parser, database -from opendc.util.exceptions import AuthorizationTokenError, RequestInitializationError - -load_dotenv() - -TEST_MODE = "OPENDC_FLASK_TESTING" in os.environ - -# Specify the directory of static assets -if TEST_MODE: - STATIC_ROOT = os.curdir -else: - STATIC_ROOT = os.path.join(os.environ['OPENDC_ROOT_DIR'], 'frontend', 'build') - -# Set up database if not testing -if not TEST_MODE: - database.DB.initialize_database( - user=os.environ['OPENDC_DB_USERNAME'], - password=os.environ['OPENDC_DB_PASSWORD'], - database=os.environ['OPENDC_DB'], - host=os.environ['OPENDC_DB_HOST'] if 'OPENDC_DB_HOST' in os.environ else 'localhost') - -# Set up the core app -FLASK_CORE_APP = Flask(__name__, static_url_path='', static_folder=STATIC_ROOT) -FLASK_CORE_APP.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] - -# Set up CORS support for local setups -if 'localhost' in os.environ['OPENDC_SERVER_BASE_URL']: - CORS(FLASK_CORE_APP) - -compress = Compress() -compress.init_app(FLASK_CORE_APP) - -if 'OPENDC_SERVER_BASE_URL' in os.environ or 'localhost' in os.environ['OPENDC_SERVER_BASE_URL']: - SOCKET_IO_CORE = flask_socketio.SocketIO(FLASK_CORE_APP, cors_allowed_origins="*") -else: - SOCKET_IO_CORE = flask_socketio.SocketIO(FLASK_CORE_APP) - - -@FLASK_CORE_APP.errorhandler(404) -def page_not_found(e): - return send_from_directory(STATIC_ROOT, 'index.html') - - -@FLASK_CORE_APP.route('/tokensignin', methods=['POST']) -def sign_in(): - """Authenticate a user with Google sign in""" - - try: - token = request.form['idtoken'] - except KeyError: - return 'No idtoken provided', 401 - - try: - idinfo = client.verify_id_token(token, os.environ['OPENDC_OAUTH_CLIENT_ID']) - - if idinfo['aud'] != os.environ['OPENDC_OAUTH_CLIENT_ID']: - raise crypt.AppIdentityError('Unrecognized client.') - - if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: - raise crypt.AppIdentityError('Wrong issuer.') - except ValueError: - url = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={}".format(token) - req = urllib.request.Request(url) - response = urllib.request.urlopen(url=req, timeout=30) - res = response.read() - idinfo = json.loads(res) - except crypt.AppIdentityError as e: - return 'Did not successfully authenticate' - - user = User.from_google_id(idinfo['sub']) - - data = {'isNewUser': user.obj is None} - - if user.obj is not None: - data['userId'] = user.get_id() - - return jsonify(**data) - - -@FLASK_CORE_APP.route('/api//', methods=['GET', 'POST', 'PUT', 'DELETE']) -def api_call(version, endpoint_path): - """Call an API endpoint directly over HTTP.""" - - # Get path and parameters - (path, path_parameters) = path_parser.parse(version, endpoint_path) - - query_parameters = request.args.to_dict() - for param in query_parameters: - try: - query_parameters[param] = int(query_parameters[param]) - except: - pass - - try: - body_parameters = json.loads(request.get_data()) - except: - body_parameters = {} - - # Create and call request - (req, response) = _process_message({ - 'id': 0, - 'method': request.method, - 'parameters': { - 'body': body_parameters, - 'path': path_parameters, - 'query': query_parameters - }, - 'path': path, - 'token': request.headers.get('auth-token') - }) - - print( - f'HTTP:\t{req.method} to `/{req.path}` resulted in {response.status["code"]}: {response.status["description"]}') - sys.stdout.flush() - - flask_response = jsonify(json.loads(response.to_JSON())) - flask_response.status_code = response.status['code'] - return flask_response - - -@FLASK_CORE_APP.route('/my-auth-token') -def serve_web_server_test(): - """Serve the web server test.""" - return send_from_directory(STATIC_ROOT, 'index.html') - - -@FLASK_CORE_APP.route('/') -@FLASK_CORE_APP.route('/projects') -@FLASK_CORE_APP.route('/projects/') -@FLASK_CORE_APP.route('/profile') -def serve_index(project_id=None): - return send_from_directory(STATIC_ROOT, 'index.html') - - -@SOCKET_IO_CORE.on('request') -def receive_message(message): - """"Receive a SocketIO request""" - (req, res) = _process_message(message) - - print(f'Socket: {req.method} to `/{req.path}` resulted in {res.status["code"]}: {res.status["description"]}') - sys.stdout.flush() - - flask_socketio.emit('response', res.to_JSON(), json=True) - - -def _process_message(message): - """Process a request message and return the response.""" - - try: - req = rest.Request(message) - res = req.process() - - return req, res - - except AuthorizationTokenError: - res = rest.Response(401, 'Authorization error') - res.id = message['id'] - - except RequestInitializationError as e: - res = rest.Response(400, str(e)) - res.id = message['id'] - - if not 'method' in message: - message['method'] = 'UNSPECIFIED' - if not 'path' in message: - message['path'] = 'UNSPECIFIED' - - except Exception: - res = rest.Response(500, 'Internal server error') - if 'id' in message: - res.id = message['id'] - traceback.print_exc() - - req = rest.Request() - req.method = message['method'] - req.path = message['path'] - - return req, res - - -if __name__ == '__main__': - print("Web server started on 8081") - SOCKET_IO_CORE.run(FLASK_CORE_APP, host='0.0.0.0', port=8081) diff --git a/web-server/misc/artwork/opendc-web-server-component-diagram.png b/web-server/misc/artwork/opendc-web-server-component-diagram.png deleted file mode 100644 index 91b26006..00000000 Binary files a/web-server/misc/artwork/opendc-web-server-component-diagram.png and /dev/null differ diff --git a/web-server/opendc/__init__.py b/web-server/opendc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/__init__.py b/web-server/opendc/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/__init__.py b/web-server/opendc/api/v2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/paths.json b/web-server/opendc/api/v2/paths.json deleted file mode 100644 index 90d5a2e6..00000000 --- a/web-server/opendc/api/v2/paths.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - "/users", - "/users/{userId}", - "/projects", - "/projects/{projectId}", - "/projects/{projectId}/authorizations", - "/projects/{projectId}/topologies", - "/projects/{projectId}/portfolios", - "/topologies/{topologyId}", - "/portfolios/{portfolioId}", - "/portfolios/{portfolioId}/scenarios", - "/scenarios/{scenarioId}", - "/schedulers", - "/traces", - "/traces/{traceId}", - "/prefabs", - "/prefabs/{prefabId}" -] diff --git a/web-server/opendc/api/v2/portfolios/__init__.py b/web-server/opendc/api/v2/portfolios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py deleted file mode 100644 index c0ca64e0..00000000 --- a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py +++ /dev/null @@ -1,65 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Get this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved portfolio.', portfolio.obj) - - -def PUT(request): - """Update this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { - 'name': 'string', - 'targets': { - 'enabledMetrics': 'list', - 'repeatsPerScenario': 'int', - }, - }}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, True) - - portfolio.set_property('name', - request.params_body['portfolio']['name']) - portfolio.set_property('targets.enabledMetrics', - request.params_body['portfolio']['targets']['enabledMetrics']) - portfolio.set_property('targets.repeatsPerScenario', - request.params_body['portfolio']['targets']['repeatsPerScenario']) - - portfolio.update() - - return Response(200, 'Successfully updated portfolio.', portfolio.obj) - - -def DELETE(request): - """Delete this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, True) - - project = Project.from_id(portfolio.obj['projectId']) - project.check_exists() - if request.params_path['portfolioId'] in project.obj['portfolioIds']: - project.obj['portfolioIds'].remove(request.params_path['portfolioId']) - project.update() - - old_object = portfolio.delete() - - return Response(200, 'Successfully deleted portfolio.', old_object) diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py deleted file mode 100644 index 1c5e0ab6..00000000 --- a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py +++ /dev/null @@ -1,43 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.scenario import Scenario -from opendc.util.rest import Response - - -def POST(request): - """Add a new Scenario for this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}, - body={ - 'scenario': { - 'name': 'string', - 'trace': { - 'traceId': 'string', - 'loadSamplingFraction': 'float', - }, - 'topology': { - 'topologyId': 'string', - }, - 'operational': { - 'failuresEnabled': 'bool', - 'performanceInterferenceEnabled': 'bool', - 'schedulerName': 'string', - }, - } - }) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, True) - - scenario = Scenario(request.params_body['scenario']) - - scenario.set_property('portfolioId', request.params_path['portfolioId']) - scenario.set_property('simulationState', 'QUEUED') - - scenario.insert() - - portfolio.obj['scenarioIds'].append(scenario.get_id()) - portfolio.update() - - return Response(200, 'Successfully added Scenario.', scenario.obj) diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py deleted file mode 100644 index 8b55bab0..00000000 --- a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py +++ /dev/null @@ -1,119 +0,0 @@ -from opendc.util.database import DB - - -def test_add_scenario_missing_parameter(client): - assert '400' in client.post('/api/v2/portfolios/1/scenarios').status - - -def test_add_scenario_non_existing_portfolio(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post('/api/v2/portfolios/1/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': '1', - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': '1', - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }).status - - -def test_add_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'portfolioId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post('/api/v2/portfolios/1/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': '1', - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': '1', - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }).status - - -def test_add_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'portfolioId': '1', - 'portfolioIds': ['1'], - 'scenarioIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }], - 'simulationState': 'QUEUED', - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'name': 'test', - 'trace': { - 'traceId': '1', - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': '1', - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - 'portfolioId': '1', - 'simulationState': 'QUEUED', - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - '/api/v2/portfolios/1/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': '1', - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': '1', - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }) - assert 'portfolioId' in res.json['content'] - assert 'simulationState' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py deleted file mode 100644 index 7ac346d4..00000000 --- a/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py +++ /dev/null @@ -1,149 +0,0 @@ -from opendc.util.database import DB - - -def test_get_portfolio_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/portfolios/1').status - - -def test_get_portfolio_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) - res = client.get('/api/v2/portfolios/1') - assert '403' in res.status - - -def test_get_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - res = client.get('/api/v2/portfolios/1') - assert '403' in res.status - - -def test_get_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get('/api/v2/portfolios/1') - assert '200' in res.status - - -def test_update_portfolio_missing_parameter(client): - assert '400' in client.put('/api/v2/portfolios/1').status - - -def test_update_portfolio_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/portfolios/1', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/portfolios/1', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }], - 'targets': { - 'enabledMetrics': [], - 'repeatsPerScenario': 1 - } - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put('/api/v2/portfolios/1', json={'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/portfolios/1').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/portfolios/1').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'googleId': 'test', - 'portfolioIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete('/api/v2/portfolios/1') - assert '200' in res.status diff --git a/web-server/opendc/api/v2/prefabs/__init__.py b/web-server/opendc/api/v2/prefabs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/prefabs/endpoint.py b/web-server/opendc/api/v2/prefabs/endpoint.py deleted file mode 100644 index 723a2f0d..00000000 --- a/web-server/opendc/api/v2/prefabs/endpoint.py +++ /dev/null @@ -1,23 +0,0 @@ -from datetime import datetime - -from opendc.models.prefab import Prefab -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def POST(request): - """Create a new prefab, and return that new prefab.""" - - request.check_required_parameters(body={'prefab': {'name': 'string'}}) - - prefab = Prefab(request.params_body['prefab']) - prefab.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) - prefab.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - - user = User.from_google_id(request.google_id) - prefab.set_property('authorId', user.get_id()) - - prefab.insert() - - return Response(200, 'Successfully created prefab.', prefab.obj) diff --git a/web-server/opendc/api/v2/prefabs/prefabId/__init__.py b/web-server/opendc/api/v2/prefabs/prefabId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/prefabs/prefabId/endpoint.py b/web-server/opendc/api/v2/prefabs/prefabId/endpoint.py deleted file mode 100644 index e8508ee0..00000000 --- a/web-server/opendc/api/v2/prefabs/prefabId/endpoint.py +++ /dev/null @@ -1,53 +0,0 @@ -from datetime import datetime - -from opendc.models.prefab import Prefab -from opendc.util.database import Database -from opendc.util.rest import Response - - -def GET(request): - """Get this Prefab.""" - - request.check_required_parameters(path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - print(prefab.obj) - prefab.check_exists() - print("before cua") - prefab.check_user_access(request.google_id) - print("after cua") - - return Response(200, 'Successfully retrieved prefab', prefab.obj) - - -def PUT(request): - """Update a prefab's name and/or contents.""" - - request.check_required_parameters(body={'prefab': {'name': 'name'}}, path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - - prefab.check_exists() - prefab.check_user_access(request.google_id) - - prefab.set_property('name', request.params_body['prefab']['name']) - prefab.set_property('rack', request.params_body['prefab']['rack']) - prefab.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now())) - prefab.update() - - return Response(200, 'Successfully updated prefab.', prefab.obj) - - -def DELETE(request): - """Delete this Prefab.""" - - request.check_required_parameters(path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - - prefab.check_exists() - prefab.check_user_access(request.google_id) - - old_object = prefab.delete() - - return Response(200, 'Successfully deleted prefab.', old_object) diff --git a/web-server/opendc/api/v2/prefabs/prefabId/test_endpoint.py b/web-server/opendc/api/v2/prefabs/prefabId/test_endpoint.py deleted file mode 100644 index b25c881d..00000000 --- a/web-server/opendc/api/v2/prefabs/prefabId/test_endpoint.py +++ /dev/null @@ -1,140 +0,0 @@ -from opendc.util.database import DB -from unittest.mock import Mock - - -def test_get_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/prefabs/1').status - -def test_get_private_prefab_not_authorized(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '2', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - res = client.get('/api/v2/prefabs/1') - assert '403' in res.status - - -def test_get_private_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '1', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - res = client.get('/api/v2/prefabs/1') - assert '200' in res.status - -def test_get_public_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '2', - 'visibility': 'public', - 'rack': {} - }, - { - '_id': '1' - } - ] - res = client.get('/api/v2/prefabs/1') - assert '200' in res.status - - -def test_update_prefab_missing_parameter(client): - assert '400' in client.put('/api/v2/prefabs/1').status - - -def test_update_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/prefabs/1', json={'prefab': {'name': 'S'}}).status - - -def test_update_prefab_not_authorized(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '2', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/prefabs/1', json={'prefab': {'name': 'test prefab', 'rack' : {}}}).status - - -def test_update_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '1', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - mocker.patch.object(DB, 'update', return_value={}) - res = client.put('/api/v2/prefabs/1', json={'prefab': {'name': 'test prefab', 'rack' : {}}}) - assert '200' in res.status - - -def test_delete_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/prefabs/1').status - - -def test_delete_prefab_different_user(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '2', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/prefabs/1').status - - -def test_delete_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': '1', - 'name': 'test prefab', - 'authorId': '1', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': '1' - } - ] - mocker.patch.object(DB, 'delete_one', return_value={'prefab': {'name': 'name'}}) - res = client.delete('/api/v2/prefabs/1') - assert '200' in res.status diff --git a/web-server/opendc/api/v2/prefabs/test_endpoint.py b/web-server/opendc/api/v2/prefabs/test_endpoint.py deleted file mode 100644 index 47029579..00000000 --- a/web-server/opendc/api/v2/prefabs/test_endpoint.py +++ /dev/null @@ -1,22 +0,0 @@ -from opendc.util.database import DB - - -def test_add_prefab_missing_parameter(client): - assert '400' in client.post('/api/v2/prefabs').status - - -def test_add_prefab(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'authorizations': []}) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': 1 - }) - res = client.post('/api/v2/prefabs', json={'prefab': {'name': 'test prefab'}}) - assert 'datetimeCreated' in res.json['content'] - assert 'datetimeLastEdited' in res.json['content'] - assert 'authorId' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/__init__.py b/web-server/opendc/api/v2/projects/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/endpoint.py b/web-server/opendc/api/v2/projects/endpoint.py deleted file mode 100644 index bf031382..00000000 --- a/web-server/opendc/api/v2/projects/endpoint.py +++ /dev/null @@ -1,32 +0,0 @@ -from datetime import datetime - -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def POST(request): - """Create a new project, and return that new project.""" - - request.check_required_parameters(body={'project': {'name': 'string'}}) - - topology = Topology({'name': 'Default topology', 'rooms': []}) - topology.insert() - - project = Project(request.params_body['project']) - project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) - project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - project.set_property('topologyIds', [topology.get_id()]) - project.set_property('portfolioIds', []) - project.insert() - - topology.set_property('projectId', project.get_id()) - topology.update() - - user = User.from_google_id(request.google_id) - user.obj['authorizations'].append({'projectId': project.get_id(), 'authorizationLevel': 'OWN'}) - user.update() - - return Response(200, 'Successfully created project.', project.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/__init__.py b/web-server/opendc/api/v2/projects/projectId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/projectId/authorizations/__init__.py b/web-server/opendc/api/v2/projects/projectId/authorizations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/projectId/authorizations/endpoint.py b/web-server/opendc/api/v2/projects/projectId/authorizations/endpoint.py deleted file mode 100644 index 9f6a60ec..00000000 --- a/web-server/opendc/api/v2/projects/projectId/authorizations/endpoint.py +++ /dev/null @@ -1,17 +0,0 @@ -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Find all authorizations for a Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, False) - - authorizations = project.get_all_authorizations() - - return Response(200, 'Successfully retrieved project authorizations', authorizations) diff --git a/web-server/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py deleted file mode 100644 index c3bbc093..00000000 --- a/web-server/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py +++ /dev/null @@ -1,40 +0,0 @@ -from opendc.util.database import DB - - -def test_get_authorizations_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - mocker.patch.object(DB, 'fetch_all', return_value=None) - assert '404' in client.get('/api/v2/projects/1/authorizations').status - - -def test_get_authorizations_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'name': 'test trace', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'fetch_all', return_value=[]) - res = client.get('/api/v2/projects/1/authorizations') - assert '403' in res.status - - -def test_get_authorizations(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'name': 'test trace', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'fetch_all', return_value=[]) - res = client.get('/api/v2/projects/1/authorizations') - assert len(res.json['content']) == 0 - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/endpoint.py b/web-server/opendc/api/v2/projects/projectId/endpoint.py deleted file mode 100644 index 77b66d75..00000000 --- a/web-server/opendc/api/v2/projects/projectId/endpoint.py +++ /dev/null @@ -1,66 +0,0 @@ -from datetime import datetime - -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def GET(request): - """Get this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved project', project.obj) - - -def PUT(request): - """Update a project's name.""" - - request.check_required_parameters(body={'project': {'name': 'name'}}, path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - project.set_property('name', request.params_body['project']['name']) - project.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now())) - project.update() - - return Response(200, 'Successfully updated project.', project.obj) - - -def DELETE(request): - """Delete this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - for topology_id in project.obj['topologyIds']: - topology = Topology.from_id(topology_id) - topology.delete() - - for portfolio_id in project.obj['portfolioIds']: - portfolio = Portfolio.from_id(portfolio_id) - portfolio.delete() - - user = User.from_google_id(request.google_id) - user.obj['authorizations'] = list( - filter(lambda x: str(x['projectId']) != request.params_path['projectId'], user.obj['authorizations'])) - user.update() - - old_object = project.delete() - - return Response(200, 'Successfully deleted project.', old_object) diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py b/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py deleted file mode 100644 index 0bc65565..00000000 --- a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py +++ /dev/null @@ -1,35 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.util.rest import Response - - -def POST(request): - """Add a new Portfolio for this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}, - body={ - 'portfolio': { - 'name': 'string', - 'targets': { - 'enabledMetrics': 'list', - 'repeatsPerScenario': 'int', - }, - } - }) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - portfolio = Portfolio(request.params_body['portfolio']) - - portfolio.set_property('projectId', request.params_path['projectId']) - portfolio.set_property('scenarioIds', []) - - portfolio.insert() - - project.obj['portfolioIds'].append(portfolio.get_id()) - project.update() - - return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py deleted file mode 100644 index 24416cc3..00000000 --- a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py +++ /dev/null @@ -1,83 +0,0 @@ -from opendc.util.database import DB - - -def test_add_portfolio_missing_parameter(client): - assert '400' in client.post('/api/v2/projects/1/portfolios').status - - -def test_add_portfolio_non_existing_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post('/api/v2/projects/1/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post('/api/v2/projects/1/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'portfolioIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - }, - 'projectId': '1', - 'scenarioIds': [], - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - '/api/v2/projects/1/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }) - assert 'projectId' in res.json['content'] - assert 'scenarioIds' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/test_endpoint.py deleted file mode 100644 index 7a862e8d..00000000 --- a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py +++ /dev/null @@ -1,119 +0,0 @@ -from opendc.util.database import DB - - -def test_get_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/projects/1').status - - -def test_get_project_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'authorizations': []}) - res = client.get('/api/v2/projects/1') - assert '403' in res.status - - -def test_get_project_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - res = client.get('/api/v2/projects/1') - assert '403' in res.status - - -def test_get_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get('/api/v2/projects/1') - assert '200' in res.status - - -def test_update_project_missing_parameter(client): - assert '400' in client.put('/api/v2/projects/1').status - - -def test_update_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}).status - - -def test_update_project_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}).status - - -def test_update_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put('/api/v2/projects/1', json={'project': {'name': 'S'}}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/projects/1').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }], - 'topologyIds': [] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/projects/1').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'googleId': 'test', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }], - 'topologyIds': [], - 'portfolioIds': [], - }) - mocker.patch.object(DB, 'update', return_value=None) - mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) - res = client.delete('/api/v2/projects/1') - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/topologies/__init__.py b/web-server/opendc/api/v2/projects/projectId/topologies/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/projects/projectId/topologies/endpoint.py b/web-server/opendc/api/v2/projects/projectId/topologies/endpoint.py deleted file mode 100644 index 211dc15d..00000000 --- a/web-server/opendc/api/v2/projects/projectId/topologies/endpoint.py +++ /dev/null @@ -1,31 +0,0 @@ -from datetime import datetime - -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.util.rest import Response -from opendc.util.database import Database - - -def POST(request): - """Add a new Topology to the specified project and return it""" - - request.check_required_parameters(path={'projectId': 'string'}, body={'topology': {'name': 'string'}}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - topology = Topology({ - 'projectId': request.params_path['projectId'], - 'name': request.params_body['topology']['name'], - 'rooms': request.params_body['topology']['rooms'], - }) - - topology.insert() - - project.obj['topologyIds'].append(topology.get_id()) - project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - project.update() - - return Response(200, 'Successfully inserted topology.', topology.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/topologies/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/topologies/test_endpoint.py deleted file mode 100644 index ca123a73..00000000 --- a/web-server/opendc/api/v2/projects/projectId/topologies/test_endpoint.py +++ /dev/null @@ -1,50 +0,0 @@ -from opendc.util.database import DB - - -def test_add_topology_missing_parameter(client): - assert '400' in client.post('/api/v2/projects/1/topologies').status - - -def test_add_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }], - 'topologyIds': [] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(DB, 'update', return_value={}) - res = client.post('/api/v2/projects/1/topologies', json={'topology': {'name': 'test project', 'rooms': []}}) - assert 'rooms' in res.json['content'] - assert '200' in res.status - - -def test_add_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post('/api/v2/projects/1/topologies', - json={ - 'topology': { - 'name': 'test_topology', - 'rooms': {} - } - }).status diff --git a/web-server/opendc/api/v2/projects/test_endpoint.py b/web-server/opendc/api/v2/projects/test_endpoint.py deleted file mode 100644 index a50735b0..00000000 --- a/web-server/opendc/api/v2/projects/test_endpoint.py +++ /dev/null @@ -1,23 +0,0 @@ -from opendc.util.database import DB - - -def test_add_project_missing_parameter(client): - assert '400' in client.post('/api/v2/projects').status - - -def test_add_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'authorizations': []}) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(DB, 'update', return_value={}) - res = client.post('/api/v2/projects', json={'project': {'name': 'test project'}}) - assert 'datetimeCreated' in res.json['content'] - assert 'datetimeLastEdited' in res.json['content'] - assert 'topologyIds' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/scenarios/__init__.py b/web-server/opendc/api/v2/scenarios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py b/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py deleted file mode 100644 index 02d39063..00000000 --- a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py +++ /dev/null @@ -1,57 +0,0 @@ -from opendc.models.scenario import Scenario -from opendc.models.portfolio import Portfolio -from opendc.util.rest import Response - - -def GET(request): - """Get this Scenario.""" - - request.check_required_parameters(path={'scenarioId': 'string'}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved scenario.', scenario.obj) - - -def PUT(request): - """Update this Scenarios name.""" - - request.check_required_parameters(path={'scenarioId': 'string'}, body={'scenario': { - 'name': 'string', - }}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, True) - - scenario.set_property('name', - request.params_body['scenario']['name']) - - scenario.update() - - return Response(200, 'Successfully updated scenario.', scenario.obj) - - -def DELETE(request): - """Delete this Scenario.""" - - request.check_required_parameters(path={'scenarioId': 'string'}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, True) - - portfolio = Portfolio.from_id(scenario.obj['portfolioId']) - portfolio.check_exists() - if request.params_path['scenarioId'] in portfolio.obj['scenarioIds']: - portfolio.obj['scenarioIds'].remove(request.params_path['scenarioId']) - portfolio.update() - - old_object = scenario.delete() - - return Response(200, 'Successfully deleted scenario.', old_object) diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py deleted file mode 100644 index 09b7d0c0..00000000 --- a/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py +++ /dev/null @@ -1,140 +0,0 @@ -from opendc.util.database import DB - - -def test_get_scenario_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/scenarios/1').status - - -def test_get_scenario_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={ - 'portfolioId': '1', - 'authorizations': [] - }) - res = client.get('/api/v2/scenarios/1') - assert '403' in res.status - - -def test_get_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'portfolioId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - res = client.get('/api/v2/scenarios/1') - assert '403' in res.status - - -def test_get_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'portfolioId': '1', - '_id': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get('/api/v2/scenarios/1') - assert '200' in res.status - - -def test_update_scenario_missing_parameter(client): - assert '400' in client.put('/api/v2/scenarios/1').status - - -def test_update_scenario_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/scenarios/1', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'portfolioId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/scenarios/1', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'portfolioId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }], - 'targets': { - 'enabledMetrics': [], - 'repeatsPerScenario': 1 - } - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put('/api/v2/scenarios/1', json={'scenario': { - 'name': 'test', - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/scenarios/1').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'portfolioId': '1', - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/scenarios/1').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'portfolioId': '1', - 'googleId': 'test', - 'scenarioIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete('/api/v2/scenarios/1') - assert '200' in res.status diff --git a/web-server/opendc/api/v2/schedulers/__init__.py b/web-server/opendc/api/v2/schedulers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/schedulers/endpoint.py b/web-server/opendc/api/v2/schedulers/endpoint.py deleted file mode 100644 index a96fdd88..00000000 --- a/web-server/opendc/api/v2/schedulers/endpoint.py +++ /dev/null @@ -1,9 +0,0 @@ -from opendc.util.rest import Response - -SCHEDULERS = ['DEFAULT'] - - -def GET(_): - """Get all available Schedulers.""" - - return Response(200, 'Successfully retrieved Schedulers.', [{'name': name} for name in SCHEDULERS]) diff --git a/web-server/opendc/api/v2/schedulers/test_endpoint.py b/web-server/opendc/api/v2/schedulers/test_endpoint.py deleted file mode 100644 index a0bd8758..00000000 --- a/web-server/opendc/api/v2/schedulers/test_endpoint.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_get_schedulers(client): - assert '200' in client.get('/api/v2/schedulers').status diff --git a/web-server/opendc/api/v2/topologies/__init__.py b/web-server/opendc/api/v2/topologies/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/topologies/topologyId/__init__.py b/web-server/opendc/api/v2/topologies/topologyId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/topologies/topologyId/endpoint.py b/web-server/opendc/api/v2/topologies/topologyId/endpoint.py deleted file mode 100644 index 512b050a..00000000 --- a/web-server/opendc/api/v2/topologies/topologyId/endpoint.py +++ /dev/null @@ -1,56 +0,0 @@ -from datetime import datetime - -from opendc.util.database import Database -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.util.rest import Response - - -def GET(request): - """Get this Topology.""" - - request.check_required_parameters(path={'topologyId': 'string'}) - - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved topology.', topology.obj) - - -def PUT(request): - """Update this topology""" - request.check_required_parameters(path={'topologyId': 'string'}, body={'topology': {'name': 'string', 'rooms': {}}}) - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, True) - - topology.set_property('name', request.params_body['topology']['name']) - topology.set_property('rooms', request.params_body['topology']['rooms']) - topology.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - - topology.update() - - return Response(200, 'Successfully updated topology.', topology.obj) - - -def DELETE(request): - """Delete this topology""" - request.check_required_parameters(path={'topologyId': 'string'}) - - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, True) - - project = Project.from_id(topology.obj['projectId']) - project.check_exists() - if request.params_path['topologyId'] in project.obj['topologyIds']: - project.obj['topologyIds'].remove(request.params_path['topologyId']) - project.update() - - old_object = topology.delete() - - return Response(200, 'Successfully deleted topology.', old_object) diff --git a/web-server/opendc/api/v2/topologies/topologyId/test_endpoint.py b/web-server/opendc/api/v2/topologies/topologyId/test_endpoint.py deleted file mode 100644 index b25cb798..00000000 --- a/web-server/opendc/api/v2/topologies/topologyId/test_endpoint.py +++ /dev/null @@ -1,116 +0,0 @@ -from opendc.util.database import DB - - -def test_get_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get('/api/v2/topologies/1') - assert '200' in res.status - - -def test_get_topology_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/topologies/1').status - - -def test_get_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '2', - 'authorizationLevel': 'OWN' - }] - }) - res = client.get('/api/v2/topologies/1') - assert '403' in res.status - - -def test_get_topology_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) - res = client.get('/api/v2/topologies/1') - assert '403' in res.status - - -def test_update_topology_missing_parameter(client): - assert '400' in client.put('/api/v2/topologies/1').status - - -def test_update_topology_non_existent(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/topologies/1', json={'topology': {'name': 'test_topology', 'rooms': {}}}).status - - -def test_update_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/topologies/1', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': {} - } - }).status - - -def test_update_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - - assert '200' in client.put('/api/v2/topologies/1', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': {} - } - }).status - - -def test_delete_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'googleId': 'test', - 'topologyIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete('/api/v2/topologies/1') - assert '200' in res.status - - -def test_delete_nonexistent_topology(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/topologies/1').status diff --git a/web-server/opendc/api/v2/traces/__init__.py b/web-server/opendc/api/v2/traces/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/traces/endpoint.py b/web-server/opendc/api/v2/traces/endpoint.py deleted file mode 100644 index ee699e02..00000000 --- a/web-server/opendc/api/v2/traces/endpoint.py +++ /dev/null @@ -1,10 +0,0 @@ -from opendc.models.trace import Trace -from opendc.util.rest import Response - - -def GET(_): - """Get all available Traces.""" - - traces = Trace.get_all() - - return Response(200, 'Successfully retrieved Traces', traces.obj) diff --git a/web-server/opendc/api/v2/traces/test_endpoint.py b/web-server/opendc/api/v2/traces/test_endpoint.py deleted file mode 100644 index 9f806085..00000000 --- a/web-server/opendc/api/v2/traces/test_endpoint.py +++ /dev/null @@ -1,6 +0,0 @@ -from opendc.util.database import DB - - -def test_get_traces(client, mocker): - mocker.patch.object(DB, 'fetch_all', return_value=[]) - assert '200' in client.get('/api/v2/traces').status diff --git a/web-server/opendc/api/v2/traces/traceId/__init__.py b/web-server/opendc/api/v2/traces/traceId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/traces/traceId/endpoint.py b/web-server/opendc/api/v2/traces/traceId/endpoint.py deleted file mode 100644 index 670f88d1..00000000 --- a/web-server/opendc/api/v2/traces/traceId/endpoint.py +++ /dev/null @@ -1,14 +0,0 @@ -from opendc.models.trace import Trace -from opendc.util.rest import Response - - -def GET(request): - """Get this Trace.""" - - request.check_required_parameters(path={'traceId': 'string'}) - - trace = Trace.from_id(request.params_path['traceId']) - - trace.check_exists() - - return Response(200, 'Successfully retrieved trace.', trace.obj) diff --git a/web-server/opendc/api/v2/traces/traceId/test_endpoint.py b/web-server/opendc/api/v2/traces/traceId/test_endpoint.py deleted file mode 100644 index 56792ca9..00000000 --- a/web-server/opendc/api/v2/traces/traceId/test_endpoint.py +++ /dev/null @@ -1,13 +0,0 @@ -from opendc.util.database import DB - - -def test_get_trace_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/traces/1').status - - -def test_get_trace(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'name': 'test trace'}) - res = client.get('/api/v2/traces/1') - assert 'name' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/users/__init__.py b/web-server/opendc/api/v2/users/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/users/endpoint.py b/web-server/opendc/api/v2/users/endpoint.py deleted file mode 100644 index 0dcf2463..00000000 --- a/web-server/opendc/api/v2/users/endpoint.py +++ /dev/null @@ -1,30 +0,0 @@ -from opendc.models.user import User -from opendc.util.rest import Response - - -def GET(request): - """Search for a User using their email address.""" - - request.check_required_parameters(query={'email': 'string'}) - - user = User.from_email(request.params_query['email']) - - user.check_exists() - - return Response(200, 'Successfully retrieved user.', user.obj) - - -def POST(request): - """Add a new User.""" - - request.check_required_parameters(body={'user': {'email': 'string'}}) - - user = User(request.params_body['user']) - user.set_property('googleId', request.google_id) - user.set_property('authorizations', []) - - user.check_already_exists() - - user.insert() - - return Response(200, 'Successfully created user.', user.obj) diff --git a/web-server/opendc/api/v2/users/test_endpoint.py b/web-server/opendc/api/v2/users/test_endpoint.py deleted file mode 100644 index d60429b3..00000000 --- a/web-server/opendc/api/v2/users/test_endpoint.py +++ /dev/null @@ -1,34 +0,0 @@ -from opendc.util.database import DB - - -def test_get_user_by_email_missing_parameter(client): - assert '400' in client.get('/api/v2/users').status - - -def test_get_user_by_email_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/users?email=test@test.com').status - - -def test_get_user_by_email(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - res = client.get('/api/v2/users?email=test@test.com') - assert 'email' in res.json['content'] - assert '200' in res.status - - -def test_add_user_missing_parameter(client): - assert '400' in client.post('/api/v2/users').status - - -def test_add_user_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - assert '409' in client.post('/api/v2/users', json={'user': {'email': 'test@test.com'}}).status - - -def test_add_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - mocker.patch.object(DB, 'insert', return_value={'email': 'test@test.com'}) - res = client.post('/api/v2/users', json={'user': {'email': 'test@test.com'}}) - assert 'email' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/users/userId/__init__.py b/web-server/opendc/api/v2/users/userId/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/api/v2/users/userId/endpoint.py b/web-server/opendc/api/v2/users/userId/endpoint.py deleted file mode 100644 index be3462c0..00000000 --- a/web-server/opendc/api/v2/users/userId/endpoint.py +++ /dev/null @@ -1,59 +0,0 @@ -from opendc.models.project import Project -from opendc.models.user import User -from opendc.util.rest import Response - - -def GET(request): - """Get this User.""" - - request.check_required_parameters(path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - - return Response(200, 'Successfully retrieved user.', user.obj) - - -def PUT(request): - """Update this User's given name and/or family name.""" - - request.check_required_parameters(body={'user': { - 'givenName': 'string', - 'familyName': 'string' - }}, - path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - user.check_correct_user(request.google_id) - - user.set_property('givenName', request.params_body['user']['givenName']) - user.set_property('familyName', request.params_body['user']['familyName']) - - user.update() - - return Response(200, 'Successfully updated user.', user.obj) - - -def DELETE(request): - """Delete this User.""" - - request.check_required_parameters(path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - user.check_correct_user(request.google_id) - - for authorization in user.obj['authorizations']: - if authorization['authorizationLevel'] != 'OWN': - continue - - project = Project.from_id(authorization['projectId']) - project.delete() - - old_object = user.delete() - - return Response(200, 'Successfully deleted user.', old_object) diff --git a/web-server/opendc/api/v2/users/userId/test_endpoint.py b/web-server/opendc/api/v2/users/userId/test_endpoint.py deleted file mode 100644 index cdff2229..00000000 --- a/web-server/opendc/api/v2/users/userId/test_endpoint.py +++ /dev/null @@ -1,53 +0,0 @@ -from opendc.util.database import DB - - -def test_get_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/users/1').status - - -def test_get_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - res = client.get('/api/v2/users/1') - assert 'email' in res.json['content'] - assert '200' in res.status - - -def test_update_user_missing_parameter(client): - assert '400' in client.put('/api/v2/users/1').status - - -def test_update_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status - - -def test_update_user_different_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'other_test'}) - assert '403' in client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status - - -def test_update_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'test'}) - mocker.patch.object(DB, 'update', return_value={'givenName': 'A', 'familyName': 'B'}) - res = client.put('/api/v2/users/1', json={'user': {'givenName': 'A', 'familyName': 'B'}}) - assert 'givenName' in res.json['content'] - assert '200' in res.status - - -def test_delete_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete('/api/v2/users/1').status - - -def test_delete_user_different_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'other_test'}) - assert '403' in client.delete('/api/v2/users/1').status - - -def test_delete_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': '1', 'googleId': 'test', 'authorizations': []}) - mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) - res = client.delete('/api/v2/users/1') - assert 'googleId' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/models/__init__.py b/web-server/opendc/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/models/model.py b/web-server/opendc/models/model.py deleted file mode 100644 index bcb833ae..00000000 --- a/web-server/opendc/models/model.py +++ /dev/null @@ -1,59 +0,0 @@ -from uuid import uuid4 - -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Model: - """Base class for all models.""" - - collection_name = '' - - @classmethod - def from_id(cls, _id): - """Fetches the document with given ID from the collection.""" - return cls(DB.fetch_one({'_id': _id}, cls.collection_name)) - - @classmethod - def get_all(cls): - """Fetches all documents from the collection.""" - return cls(DB.fetch_all({}, cls.collection_name)) - - def __init__(self, obj): - self.obj = obj - - def get_id(self): - """Returns the ID of the enclosed object.""" - return str(self.obj['_id']) - - def check_exists(self): - """Raises an error if the enclosed object does not exist.""" - if self.obj is None: - raise ClientError(Response(404, 'Not found.')) - - def set_property(self, key, value): - """Sets the given property on the enclosed object, with support for simple nested access.""" - if '.' in key: - keys = key.split('.') - self.obj[keys[0]][keys[1]] = value - else: - self.obj[key] = value - - def insert(self): - """Inserts the enclosed object and generates a UUID for it.""" - self.obj['_id'] = str(uuid4()) - DB.insert(self.obj, self.collection_name) - - def update(self): - """Updates the enclosed object and updates the internal reference to the newly inserted object.""" - DB.update(self.get_id(), self.obj, self.collection_name) - - def delete(self): - """Deletes the enclosed object in the database, if it existed.""" - if self.obj is None: - return None - - old_object = self.obj.copy() - DB.delete_one({'_id': self.get_id()}, self.collection_name) - return old_object diff --git a/web-server/opendc/models/portfolio.py b/web-server/opendc/models/portfolio.py deleted file mode 100644 index 32961b63..00000000 --- a/web-server/opendc/models/portfolio.py +++ /dev/null @@ -1,24 +0,0 @@ -from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Portfolio(Model): - """Model representing a Portfolio.""" - - collection_name = 'portfolios' - - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. - - Checks access on the parent project. - - :param google_id: The Google ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - user = User.from_google_id(google_id) - authorizations = list( - filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing portfolio.')) diff --git a/web-server/opendc/models/prefab.py b/web-server/opendc/models/prefab.py deleted file mode 100644 index 70910c4a..00000000 --- a/web-server/opendc/models/prefab.py +++ /dev/null @@ -1,26 +0,0 @@ -from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Prefab(Model): - """Model representing a Project.""" - - collection_name = 'prefabs' - - def check_user_access(self, google_id): - """Raises an error if the user with given [google_id] has insufficient access to view this prefab. - - :param google_id: The Google ID of the user. - """ - user = User.from_google_id(google_id) - - #try: - - print(self.obj) - if self.obj['authorId'] != user.get_id() and self.obj['visibility'] == "private": - raise ClientError(Response(403, "Forbidden from retrieving prefab.")) - #except KeyError: - # OpenDC-authored objects don't necessarily have an authorId - # return diff --git a/web-server/opendc/models/project.py b/web-server/opendc/models/project.py deleted file mode 100644 index b57e9f77..00000000 --- a/web-server/opendc/models/project.py +++ /dev/null @@ -1,31 +0,0 @@ -from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Project(Model): - """Model representing a Project.""" - - collection_name = 'projects' - - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. - - :param google_id: The Google ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - user = User.from_google_id(google_id) - authorizations = list(filter(lambda x: str(x['projectId']) == str(self.get_id()), - user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, "Forbidden from retrieving project.")) - - def get_all_authorizations(self): - """Get all user IDs having access to this project.""" - return [ - str(user['_id']) for user in DB.fetch_all({'authorizations': { - 'projectId': self.obj['_id'] - }}, User.collection_name) - ] diff --git a/web-server/opendc/models/scenario.py b/web-server/opendc/models/scenario.py deleted file mode 100644 index 8d53e408..00000000 --- a/web-server/opendc/models/scenario.py +++ /dev/null @@ -1,26 +0,0 @@ -from opendc.models.model import Model -from opendc.models.portfolio import Portfolio -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Scenario(Model): - """Model representing a Scenario.""" - - collection_name = 'scenarios' - - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. - - Checks access on the parent project. - - :param google_id: The Google ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - portfolio = Portfolio.from_id(self.obj['portfolioId']) - user = User.from_google_id(google_id) - authorizations = list( - filter(lambda x: str(x['projectId']) == str(portfolio.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) diff --git a/web-server/opendc/models/topology.py b/web-server/opendc/models/topology.py deleted file mode 100644 index cb4c4bab..00000000 --- a/web-server/opendc/models/topology.py +++ /dev/null @@ -1,27 +0,0 @@ -from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class Topology(Model): - """Model representing a Project.""" - - collection_name = 'topologies' - - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. - - Checks access on the parent project. - - :param google_id: The Google ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - user = User.from_google_id(google_id) - if 'projectId' not in self.obj: - raise ClientError(Response(400, 'Missing projectId in topology.')) - - authorizations = list( - filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving topology.')) diff --git a/web-server/opendc/models/trace.py b/web-server/opendc/models/trace.py deleted file mode 100644 index 2f6e4926..00000000 --- a/web-server/opendc/models/trace.py +++ /dev/null @@ -1,7 +0,0 @@ -from opendc.models.model import Model - - -class Trace(Model): - """Model representing a Trace.""" - - collection_name = 'traces' diff --git a/web-server/opendc/models/user.py b/web-server/opendc/models/user.py deleted file mode 100644 index 8e8ff945..00000000 --- a/web-server/opendc/models/user.py +++ /dev/null @@ -1,36 +0,0 @@ -from opendc.models.model import Model -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class User(Model): - """Model representing a User.""" - - collection_name = 'users' - - @classmethod - def from_email(cls, email): - """Fetches the user with given email from the collection.""" - return User(DB.fetch_one({'email': email}, User.collection_name)) - - @classmethod - def from_google_id(cls, google_id): - """Fetches the user with given Google ID from the collection.""" - return User(DB.fetch_one({'googleId': google_id}, User.collection_name)) - - def check_correct_user(self, request_google_id): - """Raises an error if a user tries to modify another user. - - :param request_google_id: - """ - if request_google_id is not None and self.obj['googleId'] != request_google_id: - raise ClientError(Response(403, f'Forbidden from editing user with ID {self.obj["_id"]}.')) - - def check_already_exists(self): - """Checks if the user already exists in the database.""" - - existing_user = DB.fetch_one({'googleId': self.obj['googleId']}, self.collection_name) - - if existing_user is not None: - raise ClientError(Response(409, 'User already exists.')) diff --git a/web-server/opendc/util/__init__.py b/web-server/opendc/util/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web-server/opendc/util/database.py b/web-server/opendc/util/database.py deleted file mode 100644 index 80cdcbab..00000000 --- a/web-server/opendc/util/database.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -import urllib.parse -from datetime import datetime - -from bson.json_util import dumps -from pymongo import MongoClient - -DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S' -CONNECTION_POOL = None - - -class Database: - """Object holding functionality for database access.""" - def __init__(self): - self.opendc_db = None - - def initialize_database(self, user, password, database, host): - """Initializes the database connection.""" - - user = urllib.parse.quote_plus(user) - password = urllib.parse.quote_plus(password) - database = urllib.parse.quote_plus(database) - host = urllib.parse.quote_plus(host) - - client = MongoClient('mongodb://%s:%s@%s/default_db?authSource=%s' % (user, password, host, database)) - self.opendc_db = client.opendc - - def fetch_one(self, query, collection): - """Uses existing mongo connection to return a single (the first) document in a collection matching the given - query as a JSON object. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - bson = getattr(self.opendc_db, collection).find_one(query) - - return self.convert_bson_to_json(bson) - - def fetch_all(self, query, collection): - """Uses existing mongo connection to return all documents matching a given query, as a list of JSON objects. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - results = [] - cursor = getattr(self.opendc_db, collection).find(query) - for doc in cursor: - results.append(self.convert_bson_to_json(doc)) - return results - - def insert(self, obj, collection): - """Updates an existing object.""" - bson = getattr(self.opendc_db, collection).insert(obj) - - return self.convert_bson_to_json(bson) - - def update(self, _id, obj, collection): - """Updates an existing object.""" - bson = getattr(self.opendc_db, collection).update({'_id': _id}, obj) - - return self.convert_bson_to_json(bson) - - def delete_one(self, query, collection): - """Deletes one object matching the given query. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - getattr(self.opendc_db, collection).delete_one(query) - - def delete_all(self, query, collection): - """Deletes all objects matching the given query. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - getattr(self.opendc_db, collection).delete_many(query) - - @staticmethod - def convert_bson_to_json(bson): - """Converts a BSON representation to JSON and returns the JSON representation.""" - json_string = dumps(bson) - return json.loads(json_string) - - @staticmethod - def datetime_to_string(datetime_to_convert): - """Return a database-compatible string representation of the given datetime object.""" - return datetime_to_convert.strftime(DATETIME_STRING_FORMAT) - - @staticmethod - def string_to_datetime(string_to_convert): - """Return a datetime corresponding to the given string representation.""" - return datetime.strptime(string_to_convert, DATETIME_STRING_FORMAT) - - -DB = Database() diff --git a/web-server/opendc/util/exceptions.py b/web-server/opendc/util/exceptions.py deleted file mode 100644 index 7724a407..00000000 --- a/web-server/opendc/util/exceptions.py +++ /dev/null @@ -1,64 +0,0 @@ -class RequestInitializationError(Exception): - """Raised when a Request cannot successfully be initialized""" - - -class UnimplementedEndpointError(RequestInitializationError): - """Raised when a Request path does not point to a module.""" - - -class MissingRequestParameterError(RequestInitializationError): - """Raised when a Request does not contain one or more required parameters.""" - - -class UnsupportedMethodError(RequestInitializationError): - """Raised when a Request does not use a supported REST method. - - The method must be in all-caps, supported by REST, and implemented by the module. - """ - - -class AuthorizationTokenError(RequestInitializationError): - """Raised when an authorization token is not correctly verified.""" - - -class ForeignKeyError(Exception): - """Raised when a foreign key constraint is not met.""" - - -class RowNotFoundError(Exception): - """Raised when a database row is not found.""" - def __init__(self, table_name): - super(RowNotFoundError, self).__init__('Row in `{}` table not found.'.format(table_name)) - - self.table_name = table_name - - -class ParameterError(Exception): - """Raised when a parameter is either missing or incorrectly typed.""" - - -class IncorrectParameterError(ParameterError): - """Raised when a parameter is of the wrong type.""" - def __init__(self, parameter_name, parameter_location): - super(IncorrectParameterError, - self).__init__('Incorrectly typed `{}` {} parameter.'.format(parameter_name, parameter_location)) - - self.parameter_name = parameter_name - self.parameter_location = parameter_location - - -class MissingParameterError(ParameterError): - """Raised when a parameter is missing.""" - def __init__(self, parameter_name, parameter_location): - super(MissingParameterError, - self).__init__('Missing required `{}` {} parameter.'.format(parameter_name, parameter_location)) - - self.parameter_name = parameter_name - self.parameter_location = parameter_location - - -class ClientError(Exception): - """Raised when a 4xx response is to be returned.""" - def __init__(self, response): - super(ClientError, self).__init__(str(response)) - self.response = response diff --git a/web-server/opendc/util/parameter_checker.py b/web-server/opendc/util/parameter_checker.py deleted file mode 100644 index 14dd1dc0..00000000 --- a/web-server/opendc/util/parameter_checker.py +++ /dev/null @@ -1,85 +0,0 @@ -from opendc.util import exceptions -from opendc.util.database import Database - - -def _missing_parameter(params_required, params_actual, parent=''): - """Recursively search for the first missing parameter.""" - - for param_name in params_required: - - if param_name not in params_actual: - return '{}.{}'.format(parent, param_name) - - param_required = params_required.get(param_name) - param_actual = params_actual.get(param_name) - - if isinstance(param_required, dict): - - param_missing = _missing_parameter(param_required, param_actual, param_name) - - if param_missing is not None: - return '{}.{}'.format(parent, param_missing) - - return None - - -def _incorrect_parameter(params_required, params_actual, parent=''): - """Recursively make sure each parameter is of the correct type.""" - - for param_name in params_required: - - param_required = params_required.get(param_name) - param_actual = params_actual.get(param_name) - - if isinstance(param_required, dict): - - param_incorrect = _incorrect_parameter(param_required, param_actual, param_name) - - if param_incorrect is not None: - return '{}.{}'.format(parent, param_incorrect) - - else: - - if param_required == 'datetime': - try: - Database.string_to_datetime(param_actual) - except: - return '{}.{}'.format(parent, param_name) - - type_pairs = [ - ('int', (int,)), - ('float', (float, int)), - ('bool', (bool,)), - ('string', (str, int)), - ('list', (list,)), - ] - - for str_type, actual_types in type_pairs: - if param_required == str_type and all(not isinstance(param_actual, t) - for t in actual_types): - return '{}.{}'.format(parent, param_name) - - return None - - -def _format_parameter(parameter): - """Format the output of a parameter check.""" - - parts = parameter.split('.') - inner = ['["{}"]'.format(x) for x in parts[2:]] - return parts[1] + ''.join(inner) - - -def check(request, **kwargs): - """Check if all required parameters are there.""" - - for location, params_required in kwargs.items(): - params_actual = getattr(request, 'params_{}'.format(location)) - - missing_parameter = _missing_parameter(params_required, params_actual) - if missing_parameter is not None: - raise exceptions.MissingParameterError(_format_parameter(missing_parameter), location) - - incorrect_parameter = _incorrect_parameter(params_required, params_actual) - if incorrect_parameter is not None: - raise exceptions.IncorrectParameterError(_format_parameter(incorrect_parameter), location) diff --git a/web-server/opendc/util/path_parser.py b/web-server/opendc/util/path_parser.py deleted file mode 100644 index a8bbdeba..00000000 --- a/web-server/opendc/util/path_parser.py +++ /dev/null @@ -1,39 +0,0 @@ -import json -import os - - -def parse(version, endpoint_path): - """Map an HTTP endpoint path to an API path""" - - # Get possible paths - with open(os.path.join(os.path.dirname(__file__), '..', 'api', '{}', 'paths.json').format(version)) as paths_file: - paths = json.load(paths_file) - - # Find API path that matches endpoint_path - endpoint_path_parts = endpoint_path.strip('/').split('/') - paths_parts = [x.strip('/').split('/') for x in paths if len(x.strip('/').split('/')) == len(endpoint_path_parts)] - path = None - - for path_parts in paths_parts: - found = True - for (endpoint_part, part) in zip(endpoint_path_parts, path_parts): - if not part.startswith('{') and endpoint_part != part: - found = False - break - if found: - path = path_parts - - if path is None: - return None - - # Extract path parameters - parameters = {} - - for (name, value) in zip(path, endpoint_path_parts): - if name.startswith('{'): - try: - parameters[name.strip('{}')] = int(value) - except: - parameters[name.strip('{}')] = value - - return '{}/{}'.format(version, '/'.join(path)), parameters diff --git a/web-server/opendc/util/rest.py b/web-server/opendc/util/rest.py deleted file mode 100644 index abd2f3de..00000000 --- a/web-server/opendc/util/rest.py +++ /dev/null @@ -1,141 +0,0 @@ -import importlib -import json -import os - -from oauth2client import client, crypt - -from opendc.util import exceptions, parameter_checker -from opendc.util.exceptions import ClientError - - -class Request: - """WebSocket message to REST request mapping.""" - def __init__(self, message=None): - """"Initialize a Request from a socket message.""" - - # Get the Request parameters from the message - - if message is None: - return - - try: - self.message = message - - self.id = message['id'] - - self.path = message['path'] - self.method = message['method'] - - self.params_body = message['parameters']['body'] - self.params_path = message['parameters']['path'] - self.params_query = message['parameters']['query'] - - self.token = message['token'] - - except KeyError as exception: - raise exceptions.MissingRequestParameterError(exception) - - # Parse the path and import the appropriate module - - try: - self.path = message['path'].strip('/') - - module_base = 'opendc.api.{}.endpoint' - module_path = self.path.replace('{', '').replace('}', '').replace('/', '.') - - self.module = importlib.import_module(module_base.format(module_path)) - except ImportError as e: - print(e) - raise exceptions.UnimplementedEndpointError('Unimplemented endpoint: {}.'.format(self.path)) - - # Check the method - - if self.method not in ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']: - raise exceptions.UnsupportedMethodError('Non-rest method: {}'.format(self.method)) - - if not hasattr(self.module, self.method): - raise exceptions.UnsupportedMethodError('Unimplemented method at endpoint {}: {}'.format( - self.path, self.method)) - - # Verify the user - - if "OPENDC_FLASK_TESTING" in os.environ: - self.google_id = 'test' - return - - try: - self.google_id = self._verify_token(self.token) - except crypt.AppIdentityError as e: - raise exceptions.AuthorizationTokenError(e) - - def check_required_parameters(self, **kwargs): - """Raise an error if a parameter is missing or of the wrong type.""" - - try: - parameter_checker.check(self, **kwargs) - except exceptions.ParameterError as e: - raise ClientError(Response(400, str(e))) - - def process(self): - """Process the Request and return a Response.""" - - method = getattr(self.module, self.method) - - try: - response = method(self) - except ClientError as e: - e.response.id = self.id - return e.response - - response.id = self.id - - return response - - def to_JSON(self): - """Return a JSON representation of this Request""" - - self.message['id'] = 0 - self.message['token'] = None - - return json.dumps(self.message) - - @staticmethod - def _verify_token(token): - """Return the ID of the signed-in user. - - Or throw an Exception if the token is invalid. - """ - - try: - id_info = client.verify_id_token(token, os.environ['OPENDC_OAUTH_CLIENT_ID']) - except Exception as e: - print(e) - raise crypt.AppIdentityError('Exception caught trying to verify ID token: {}'.format(e)) - - if id_info['aud'] != os.environ['OPENDC_OAUTH_CLIENT_ID']: - raise crypt.AppIdentityError('Unrecognized client.') - - if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: - raise crypt.AppIdentityError('Wrong issuer.') - - return id_info['sub'] - - -class Response: - """Response to websocket mapping""" - def __init__(self, status_code, status_description, content=None): - """Initialize a new Response.""" - - self.id = 0 - self.status = {'code': status_code, 'description': status_description} - self.content = content - - def to_JSON(self): - """"Return a JSON representation of this Response""" - - data = {'id': self.id, 'status': self.status} - - if self.content is not None: - data['content'] = self.content - - return json.dumps(data) diff --git a/web-server/pytest.ini b/web-server/pytest.ini deleted file mode 100644 index 775a8ff4..00000000 --- a/web-server/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -env = - OPENDC_FLASK_TESTING=True - OPENDC_FLASK_SECRET=Secret - OPENDC_SERVER_BASE_URL=localhost diff --git a/web-server/requirements.txt b/web-server/requirements.txt deleted file mode 100644 index 140a046f..00000000 --- a/web-server/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -flask==1.1.2 -flask-socketio==4.3.0 -oauth2client==4.1.3 -eventlet==0.25.2 -flask-compress==1.5.0 -flask-cors==3.0.8 -pyasn1-modules==0.2.8 -six==1.15.0 -pymongo==3.10.1 -yapf==0.30.0 -pytest==5.4.3 -pytest-mock==3.1.1 -pytest-env==0.6.2 -pylint==2.5.3 -python-dotenv==0.13.0 diff --git a/web-server/static/index.html b/web-server/static/index.html deleted file mode 100644 index ac78cbfb..00000000 --- a/web-server/static/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - -
-Sign out - -

Your auth token:

-

Loading...

\ No newline at end of file -- cgit v1.2.3 From a196ba2c08bd16479134ab542f2560b75f19424f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 15 Jul 2020 15:45:02 +0200 Subject: Make frontend independent of API This change makes the frontend independent of the API by removing the static file serving logic from the API server. Instead, we can serve the frontend as static HTML over CDNs. --- .editorconfig | 4 +++ api/main.py | 36 +++++---------------------- docker-compose.yml | 61 ++++++++++++++++++++++++++-------------------- frontend/Dockerfile | 19 +++++++++++++++ frontend/nginx.conf | 32 ++++++++++++++++++++++++ frontend/src/api/socket.js | 8 +++--- frontend/yarn.lock | 2 +- 7 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf diff --git a/.editorconfig b/.editorconfig index 508cb765..b0ec2ebd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,7 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +# Ensure YAML formatting is according to standard +[*.{yml,yaml}] +indent_size = 2 diff --git a/api/main.py b/api/main.py index a2481269..7544333a 100755 --- a/api/main.py +++ b/api/main.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 -import flask_socketio import json import os import sys import traceback import urllib.request -from flask import Flask, request, send_from_directory, jsonify + +import flask_socketio +from dotenv import load_dotenv +from flask import Flask, request, jsonify from flask_compress import Compress -from oauth2client import client, crypt from flask_cors import CORS -from dotenv import load_dotenv +from oauth2client import client, crypt from opendc.models.user import User from opendc.util import rest, path_parser, database @@ -19,12 +20,6 @@ load_dotenv() TEST_MODE = "OPENDC_FLASK_TESTING" in os.environ -# Specify the directory of static assets -if TEST_MODE: - STATIC_ROOT = os.curdir -else: - STATIC_ROOT = os.path.join(os.environ['OPENDC_ROOT_DIR'], 'frontend', 'build') - # Set up database if not testing if not TEST_MODE: database.DB.initialize_database( @@ -34,7 +29,7 @@ if not TEST_MODE: host=os.environ['OPENDC_DB_HOST'] if 'OPENDC_DB_HOST' in os.environ else 'localhost') # Set up the core app -FLASK_CORE_APP = Flask(__name__, static_url_path='', static_folder=STATIC_ROOT) +FLASK_CORE_APP = Flask(__name__) FLASK_CORE_APP.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] # Set up CORS support for local setups @@ -50,11 +45,6 @@ else: SOCKET_IO_CORE = flask_socketio.SocketIO(FLASK_CORE_APP) -@FLASK_CORE_APP.errorhandler(404) -def page_not_found(e): - return send_from_directory(STATIC_ROOT, 'index.html') - - @FLASK_CORE_APP.route('/tokensignin', methods=['POST']) def sign_in(): """Authenticate a user with Google sign in""" @@ -132,20 +122,6 @@ def api_call(version, endpoint_path): return flask_response -@FLASK_CORE_APP.route('/my-auth-token') -def serve_web_server_test(): - """Serve the web server test.""" - return send_from_directory(STATIC_ROOT, 'index.html') - - -@FLASK_CORE_APP.route('/') -@FLASK_CORE_APP.route('/projects') -@FLASK_CORE_APP.route('/projects/') -@FLASK_CORE_APP.route('/profile') -def serve_index(project_id=None): - return send_from_directory(STATIC_ROOT, 'index.html') - - @SOCKET_IO_CORE.on('request') def receive_message(message): """"Receive a SocketIO request""" diff --git a/docker-compose.yml b/docker-compose.yml index 6dc01f67..837f9019 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,21 @@ version: "3" services: + frontend: + build: + context: ./frontend + args: + - REACT_APP_OAUTH_CLIENT_ID=${OPENDC_OAUTH_CLIENT_ID} + image: frontend + restart: on-failure + ports: + - "8081:80" + networks: + - backend + api: build: ./api image: api restart: on-failure - ports: - - "8081:8081" networks: - backend depends_on: @@ -20,34 +30,33 @@ services: - OPENDC_DB_HOST=mongo - OPENDC_FLASK_SECRET - OPENDC_OAUTH_CLIENT_ID - - REACT_APP_OAUTH_CLIENT_ID=${OPENDC_OAUTH_CLIENT_ID} - OPENDC_ROOT_DIR - OPENDC_SERVER_BASE_URL -# TODO: Implement new database interaction on the simulator side -# simulator: -# build: -# context: ./opendc-simulator -# dockerfile: opendc-model-odc/setup/Dockerfile -# image: simulator -# restart: on-failure -# links: -# - mongo -# depends_on: -# - mongo -# environment: -# - PERSISTENCE_URL=jdbc:mysql://mariadb:3306/opendc -# - PERSISTENCE_USER=opendc -# - PERSISTENCE_PASSWORD=opendcpassword -# - COLLECT_MACHINE_STATES=ON -# - COLLECT_TASK_STATES=ON -# - COLLECT_STAGE_MEASUREMENTS=OFF -# - COLLECT_TASK_METRICS=OFF -# - COLLECT_JOB_METRICS=OFF + # TODO: Implement new database interaction on the simulator side + # simulator: + # build: + # context: ./opendc-simulator + # dockerfile: opendc-model-odc/setup/Dockerfile + # image: simulator + # restart: on-failure + # links: + # - mongo + # depends_on: + # - mongo + # environment: + # - PERSISTENCE_URL=jdbc:mysql://mariadb:3306/opendc + # - PERSISTENCE_USER=opendc + # - PERSISTENCE_PASSWORD=opendcpassword + # - COLLECT_MACHINE_STATES=ON + # - COLLECT_TASK_STATES=ON + # - COLLECT_STAGE_MEASUREMENTS=OFF + # - COLLECT_TASK_METRICS=OFF + # - COLLECT_JOB_METRICS=OFF mongo: build: - context: database + context: database restart: on-failure environment: - MONGO_INITDB_ROOT_USERNAME @@ -79,8 +88,8 @@ services: ME_CONFIG_MONGODB_ADMINPASSWORD: "${MONGO_INITDB_ROOT_PASSWORD}" volumes: - mongo-volume: - external: false + mongo-volume: + external: false networks: backend: {} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..36e3c20b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:14 +MAINTAINER OpenDC Maintainers + +ARG REACT_APP_OAUTH_CLIENT_ID + +# Copy OpenDC directory +COPY ./ /opendc + +# Build frontend +RUN cd /opendc/ \ + && rm -rf ./build \ + && yarn \ + && export REACT_APP_OAUTH_CLIENT_ID=$REACT_APP_OAUTH_CLIENT_ID \ + && yarn build + +# Setup nginx to serve the frontend +FROM nginx:1.19 +COPY --from=0 /opendc/build /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..ed7e5cfe --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name opendc.org; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /socket.io { + proxy_http_version 1.1; + + proxy_buffering off; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_pass http://api:8081/socket.io; + } + + location /tokensignin { + proxy_pass http://api:8081/tokensignin; + } + + location /api { + proxy_pass http://api:8081/api; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/frontend/src/api/socket.js b/frontend/src/api/socket.js index 93ce8fa8..759c119e 100644 --- a/frontend/src/api/socket.js +++ b/frontend/src/api/socket.js @@ -6,11 +6,9 @@ let requestIdCounter = 0 const callbacks = {} export function setupSocketConnection(onConnect) { - let port = window.location.port - if (process.env.NODE_ENV !== 'production') { - port = 8081 - } - socket = io.connect(window.location.protocol + '//' + window.location.hostname + ':' + port) + const apiUrl = process.env.REACT_APP_API_URL || window.location.hostname + ':' + window.location.port; + + socket = io.connect(window.location.protocol + '//' + apiUrl); socket.on('connect', onConnect) socket.on('response', onSocketResponse) } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 00c6e441..2859e4e0 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -11527,7 +11527,7 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuidv4@^6.1.1: +uuidv4@~6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.1.1.tgz#6565b4f2be7d6f841c14106f420fdb701eae5c81" integrity sha512-ZplGb1SHFMVH3l7PUQl2Uwo+FpJQV6IPOoU+MjjbqrNYQolqbGwv+/sn9F+AGMsMOgGz3r9JN3ztGUi0VzMxmw== -- cgit v1.2.3 From a4ae44e7f5bbfb293cdce256da3c40f927605ac9 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 15 Jul 2020 18:02:43 +0200 Subject: Add skeleton for web runner --- .../opendc/opendc-runner-web/build.gradle.kts | 46 ++++++++++++++++++++ .../kotlin/com/atlarge/opendc/runner/web/Main.kt | 20 +++++++++ .../src/main/resources/log4j2.xml | 49 ++++++++++++++++++++++ simulator/settings.gradle.kts | 1 + 4 files changed, 116 insertions(+) create mode 100644 simulator/opendc/opendc-runner-web/build.gradle.kts create mode 100644 simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt create mode 100644 simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml diff --git a/simulator/opendc/opendc-runner-web/build.gradle.kts b/simulator/opendc/opendc-runner-web/build.gradle.kts new file mode 100644 index 00000000..50789789 --- /dev/null +++ b/simulator/opendc/opendc-runner-web/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2020 atlarge-research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Experiment runner for OpenDC" + +/* Build configuration */ +plugins { + `kotlin-library-convention` + application +} + +application { + mainClassName = "com.atlarge.opendc.runner.web.MainKt" +} + +dependencies { + api(project(":opendc:opendc-core")) + + implementation("com.github.ajalt:clikt:2.8.0") + implementation("io.github.microutils:kotlin-logging:1.7.10") + implementation("org.mongodb:mongo-java-driver:3.12.6") + + runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1") + runtimeOnly(project(":odcsim:odcsim-engine-omega")) +} diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt new file mode 100644 index 00000000..3cee259c --- /dev/null +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt @@ -0,0 +1,20 @@ +package com.atlarge.opendc.runner.web + +import com.github.ajalt.clikt.core.CliktCommand +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +/** + * Represents the CLI command for starting the OpenDC web runner. + */ +class RunnerCli : CliktCommand(name = "runner") { + override fun run() { + logger.info { "Starting OpenDC web runner" } + } +} + +/** + * Main entry point of the runner. + */ +fun main(args: Array) = RunnerCli().main(args) diff --git a/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml b/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml new file mode 100644 index 00000000..b5a2bbb5 --- /dev/null +++ b/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/simulator/settings.gradle.kts b/simulator/settings.gradle.kts index 677a9817..9411d882 100644 --- a/simulator/settings.gradle.kts +++ b/simulator/settings.gradle.kts @@ -31,3 +31,4 @@ include(":opendc:opendc-format") include(":opendc:opendc-workflows") include(":opendc:opendc-experiments-sc18") include(":opendc:opendc-experiments-sc20") +include(":opendc:opendc-runner-web") -- cgit v1.2.3 From 5d528f6b1902d372eb2ef594bc96712ad74ac361 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 16 Jul 2020 22:04:35 +0200 Subject: Add prototype of web experiment runner This change adds a bridge between the frontend and the new simulator implementation via MongoDB. --- .../portfolios/portfolioId/scenarios/endpoint.py | 2 +- .../portfolioId/scenarios/test_endpoint.py | 10 +- frontend/src/shapes/index.js | 4 +- opendc-api-spec.yml | 7 +- simulator/.editorconfig | 2 +- .../kotlin/com/atlarge/odcsim/flow/EventFlow.kt | 2 +- .../com/atlarge/opendc/compute/core/Server.kt | 2 +- .../com/atlarge/opendc/compute/metal/Node.kt | 2 +- .../opendc/compute/metal/driver/BareMetalDriver.kt | 2 +- .../compute/metal/driver/SimpleBareMetalDriver.kt | 20 +- .../com/atlarge/opendc/compute/virt/Hypervisor.kt | 2 +- .../atlarge/opendc/compute/virt/HypervisorImage.kt | 2 +- .../opendc/compute/virt/driver/SimpleVirtDriver.kt | 12 +- .../opendc/compute/virt/driver/VirtDriver.kt | 2 +- .../virt/service/SimpleVirtProvisioningService.kt | 8 +- .../core/image/FlopsApplicationImageTest.kt | 2 +- .../metal/driver/SimpleBareMetalDriverTest.kt | 4 +- .../metal/service/SimpleProvisioningServiceTest.kt | 4 +- .../atlarge/opendc/compute/virt/HypervisorTest.kt | 6 +- .../opendc/core/failure/CorrelatedFaultInjector.kt | 8 +- .../core/failure/UncorrelatedFaultInjector.kt | 4 +- .../opendc/experiments/sc18/TestExperiment.kt | 4 +- .../com/atlarge/opendc/experiments/sc20/Main.kt | 2 +- .../sc20/experiment/ExperimentHelpers.kt | 25 +- .../opendc/experiments/sc20/experiment/Run.kt | 12 +- .../experiment/monitor/ParquetExperimentMonitor.kt | 14 +- .../execution/ThreadPoolExperimentScheduler.kt | 2 +- .../runner/internal/DefaultExperimentRunner.kt | 2 +- .../sc20/telemetry/parquet/ParquetEventWriter.kt | 10 +- .../telemetry/parquet/ParquetHostEventWriter.kt | 2 +- .../parquet/ParquetProvisionerEventWriter.kt | 2 +- .../telemetry/parquet/ParquetRunEventWriter.kt | 2 +- .../sc20/trace/Sc20RawParquetTraceReader.kt | 4 +- .../sc20/trace/Sc20StreamingParquetTraceReader.kt | 16 +- .../experiments/sc20/trace/Sc20TraceConverter.kt | 12 +- .../experiments/sc20/trace/WorkloadSampler.kt | 2 +- .../opendc/experiments/sc20/Sc20IntegrationTest.kt | 4 +- .../environment/sc18/Sc18EnvironmentReader.kt | 2 +- .../format/trace/bitbrains/BitbrainsTraceReader.kt | 4 +- .../opendc/format/trace/sc20/Sc20TraceReader.kt | 4 +- .../opendc/format/trace/swf/SwfTraceReaderTest.kt | 2 +- .../opendc/opendc-runner-web/build.gradle.kts | 5 +- .../kotlin/com/atlarge/opendc/runner/web/Main.kt | 285 ++++++++++++++++++++- .../atlarge/opendc/runner/web/ScenarioManager.kt | 77 ++++++ .../atlarge/opendc/runner/web/TopologyParser.kt | 127 +++++++++ .../workflows/service/StageWorkflowService.kt | 4 +- .../opendc/workflows/service/WorkflowService.kt | 2 +- .../StageWorkflowSchedulerIntegrationTest.kt | 4 +- 48 files changed, 621 insertions(+), 117 deletions(-) create mode 100644 simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt create mode 100644 simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/TopologyParser.kt diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py index 1c5e0ab6..ca1db36a 100644 --- a/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py +++ b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py @@ -33,7 +33,7 @@ def POST(request): scenario = Scenario(request.params_body['scenario']) scenario.set_property('portfolioId', request.params_path['portfolioId']) - scenario.set_property('simulationState', 'QUEUED') + scenario.set_property('simulation', {'state': 'QUEUED'}) scenario.insert() diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py index 8b55bab0..329e68e8 100644 --- a/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py +++ b/api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py @@ -72,7 +72,9 @@ def test_add_scenario(client, mocker): 'projectId': '1', 'authorizationLevel': 'EDIT' }], - 'simulationState': 'QUEUED', + 'simulation': { + 'state': 'QUEUED', + }, }) mocker.patch.object(DB, 'insert', @@ -92,7 +94,9 @@ def test_add_scenario(client, mocker): 'schedulerName': 'DEFAULT', }, 'portfolioId': '1', - 'simulationState': 'QUEUED', + 'simulationState': { + 'state': 'QUEUED', + }, }) mocker.patch.object(DB, 'update', return_value=None) res = client.post( @@ -115,5 +119,5 @@ def test_add_scenario(client, mocker): } }) assert 'portfolioId' in res.json['content'] - assert 'simulationState' in res.json['content'] + assert 'simulation' in res.json['content'] assert '200' in res.status diff --git a/frontend/src/shapes/index.js b/frontend/src/shapes/index.js index 32914f25..8296055a 100644 --- a/frontend/src/shapes/index.js +++ b/frontend/src/shapes/index.js @@ -111,7 +111,9 @@ Shapes.Scenario = PropTypes.shape({ _id: PropTypes.string.isRequired, portfolioId: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - simulationState: PropTypes.string.isRequired, + simulation: PropTypes.shape({ + state: PropTypes.string.isRequired, + }).isRequired, trace: PropTypes.shape({ traceId: PropTypes.string.isRequired, trace: Shapes.Trace, diff --git a/opendc-api-spec.yml b/opendc-api-spec.yml index 39cb4f1c..36ef3c83 100644 --- a/opendc-api-spec.yml +++ b/opendc-api-spec.yml @@ -860,8 +860,11 @@ definitions: type: string name: type: string - simulationState: - type: string + simulation: + type: object + properties: + state: + type: string trace: type: object properties: diff --git a/simulator/.editorconfig b/simulator/.editorconfig index a17544c9..a5584e95 100644 --- a/simulator/.editorconfig +++ b/simulator/.editorconfig @@ -4,4 +4,4 @@ # ktlint [*.{kt, kts}] -disabled_rules = import-ordering +disabled_rules = no-wildcard-imports diff --git a/simulator/odcsim/odcsim-api/src/main/kotlin/com/atlarge/odcsim/flow/EventFlow.kt b/simulator/odcsim/odcsim-api/src/main/kotlin/com/atlarge/odcsim/flow/EventFlow.kt index 5d9af9ec..0e18f82f 100644 --- a/simulator/odcsim/odcsim-api/src/main/kotlin/com/atlarge/odcsim/flow/EventFlow.kt +++ b/simulator/odcsim/odcsim-api/src/main/kotlin/com/atlarge/odcsim/flow/EventFlow.kt @@ -24,6 +24,7 @@ package com.atlarge.odcsim.flow +import java.util.WeakHashMap import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.InternalCoroutinesApi @@ -32,7 +33,6 @@ import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.consumeAsFlow -import java.util.WeakHashMap /** * A [Flow] that can be used to emit events. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt index 01968cd8..fd0fc836 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt @@ -28,8 +28,8 @@ import com.atlarge.opendc.compute.core.image.Image import com.atlarge.opendc.core.resource.Resource import com.atlarge.opendc.core.resource.TagContainer import com.atlarge.opendc.core.services.ServiceRegistry -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A server instance that is running on some physical or virtual machine. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt index 7cb4c0c5..cb637aea 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt @@ -27,8 +27,8 @@ package com.atlarge.opendc.compute.metal import com.atlarge.opendc.compute.core.Server import com.atlarge.opendc.compute.core.image.Image import com.atlarge.opendc.core.Identity -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A bare-metal compute node. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt index 41cec291..17d8ee53 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt @@ -30,8 +30,8 @@ import com.atlarge.opendc.compute.metal.Node import com.atlarge.opendc.core.failure.FailureDomain import com.atlarge.opendc.core.power.Powerable import com.atlarge.opendc.core.services.AbstractServiceKey -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A driver interface for the management interface of a bare-metal compute node. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt index 6a77415c..a453e459 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt @@ -28,10 +28,10 @@ import com.atlarge.odcsim.Domain import com.atlarge.odcsim.SimulationContext import com.atlarge.odcsim.flow.EventFlow import com.atlarge.odcsim.flow.StateFlow -import com.atlarge.opendc.compute.core.ProcessingUnit -import com.atlarge.opendc.compute.core.Server import com.atlarge.opendc.compute.core.Flavor import com.atlarge.opendc.compute.core.MemoryUnit +import com.atlarge.opendc.compute.core.ProcessingUnit +import com.atlarge.opendc.compute.core.Server import com.atlarge.opendc.compute.core.ServerEvent import com.atlarge.opendc.compute.core.ServerState import com.atlarge.opendc.compute.core.execution.ServerContext @@ -46,6 +46,14 @@ import com.atlarge.opendc.compute.metal.power.ConstantPowerModel import com.atlarge.opendc.core.power.PowerModel import com.atlarge.opendc.core.services.ServiceKey import com.atlarge.opendc.core.services.ServiceRegistry +import java.lang.Exception +import java.time.Clock +import java.util.UUID +import kotlin.coroutines.ContinuationInterceptor +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.random.Random import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Delay import kotlinx.coroutines.DisposableHandle @@ -59,15 +67,7 @@ import kotlinx.coroutines.intrinsics.startCoroutineCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.selects.SelectClause0 import kotlinx.coroutines.selects.SelectInstance -import java.util.UUID -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min import kotlinx.coroutines.withContext -import java.lang.Exception -import java.time.Clock -import kotlin.coroutines.ContinuationInterceptor -import kotlin.random.Random /** * A basic implementation of the [BareMetalDriver] that simulates an [Image] running on a bare-metal machine. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt index 69b0124d..1e7e351f 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt @@ -25,8 +25,8 @@ package com.atlarge.opendc.compute.virt import com.atlarge.opendc.core.Identity -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A hypervisor (or virtual machine monitor) is software or firmware that virtualizes the host compute environment diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt index bd395f0d..607759a8 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt @@ -29,9 +29,9 @@ import com.atlarge.opendc.compute.core.image.Image import com.atlarge.opendc.compute.virt.driver.SimpleVirtDriver import com.atlarge.opendc.compute.virt.driver.VirtDriver import com.atlarge.opendc.core.resource.TagContainer +import java.util.UUID import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.suspendCancellableCoroutine -import java.util.UUID /** * A hypervisor managing the VMs of a node. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt index 3c41f52e..192db413 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt @@ -35,11 +35,15 @@ import com.atlarge.opendc.compute.core.execution.ServerContext import com.atlarge.opendc.compute.core.execution.ServerManagementContext import com.atlarge.opendc.compute.core.execution.ShutdownException import com.atlarge.opendc.compute.core.image.Image +import com.atlarge.opendc.compute.core.workload.IMAGE_PERF_INTERFERENCE_MODEL +import com.atlarge.opendc.compute.core.workload.PerformanceInterferenceModel import com.atlarge.opendc.compute.virt.HypervisorEvent import com.atlarge.opendc.core.services.ServiceKey import com.atlarge.opendc.core.services.ServiceRegistry -import com.atlarge.opendc.compute.core.workload.IMAGE_PERF_INTERFERENCE_MODEL -import com.atlarge.opendc.compute.core.workload.PerformanceInterferenceModel +import java.util.UUID +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle @@ -55,10 +59,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.selects.SelectClause0 import kotlinx.coroutines.selects.SelectInstance import kotlinx.coroutines.selects.select -import java.util.UUID -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min /** * A [VirtDriver] that is backed by a simple hypervisor implementation. diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt index 1002d382..b1844f67 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt @@ -29,8 +29,8 @@ import com.atlarge.opendc.compute.core.Server import com.atlarge.opendc.compute.core.image.Image import com.atlarge.opendc.compute.virt.HypervisorEvent import com.atlarge.opendc.core.services.AbstractServiceKey -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A driver interface for a hypervisor running on some host server and communicating with the central compute service to diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt index ff4aa3d7..79388bc3 100644 --- a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt +++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt @@ -11,11 +11,14 @@ import com.atlarge.opendc.compute.core.image.Image import com.atlarge.opendc.compute.core.image.VmImage import com.atlarge.opendc.compute.metal.service.ProvisioningService import com.atlarge.opendc.compute.virt.HypervisorEvent -import com.atlarge.opendc.compute.virt.driver.VirtDriver import com.atlarge.opendc.compute.virt.HypervisorImage import com.atlarge.opendc.compute.virt.driver.InsufficientMemoryOnServerException +import com.atlarge.opendc.compute.virt.driver.VirtDriver import com.atlarge.opendc.compute.virt.service.allocation.AllocationPolicy import com.atlarge.opendc.core.services.ServiceKey +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -27,9 +30,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import mu.KotlinLogging -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.math.max private val logger = KotlinLogging.logger {} diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt index 417db77d..1c7b751c 100644 --- a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt +++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt @@ -24,10 +24,10 @@ package com.atlarge.opendc.compute.core.image +import java.util.UUID import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.util.UUID /** * Test suite for [FlopsApplicationImage] diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt index 071c0626..af9d3421 100644 --- a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt +++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt @@ -31,6 +31,8 @@ import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.core.ServerEvent import com.atlarge.opendc.compute.core.ServerState import com.atlarge.opendc.compute.core.image.FlopsApplicationImage +import java.util.ServiceLoader +import java.util.UUID import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -39,8 +41,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import java.util.ServiceLoader -import java.util.UUID internal class SimpleBareMetalDriverTest { /** diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt index f8bd786e..ed2256c0 100644 --- a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt +++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt @@ -29,13 +29,13 @@ import com.atlarge.opendc.compute.core.ProcessingNode import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.core.image.FlopsApplicationImage import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver +import java.util.ServiceLoader +import java.util.UUID import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test -import java.util.ServiceLoader -import java.util.UUID /** * Test suite for the [SimpleProvisioningService]. diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt index ca00fc94..622b185e 100644 --- a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt +++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt @@ -25,14 +25,16 @@ package com.atlarge.opendc.compute.virt import com.atlarge.odcsim.SimulationEngineProvider -import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.core.Flavor import com.atlarge.opendc.compute.core.ProcessingNode +import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.core.image.FlopsApplicationImage import com.atlarge.opendc.compute.core.image.FlopsHistoryFragment import com.atlarge.opendc.compute.core.image.VmImage import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver import com.atlarge.opendc.compute.virt.driver.VirtDriver +import java.util.ServiceLoader +import java.util.UUID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -43,8 +45,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll -import java.util.ServiceLoader -import java.util.UUID /** * Basic test-suite for the hypervisor. diff --git a/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/CorrelatedFaultInjector.kt b/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/CorrelatedFaultInjector.kt index 50261db5..f77a581e 100644 --- a/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/CorrelatedFaultInjector.kt +++ b/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/CorrelatedFaultInjector.kt @@ -26,14 +26,14 @@ package com.atlarge.opendc.core.failure import com.atlarge.odcsim.Domain import com.atlarge.odcsim.simulationContext -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch import kotlin.math.exp import kotlin.math.max import kotlin.random.Random import kotlin.random.asJavaRandom +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch /** * A [FaultInjector] that injects fault in the system which are correlated to each other. Failures do not occur in diff --git a/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/UncorrelatedFaultInjector.kt b/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/UncorrelatedFaultInjector.kt index 1b896858..0f62667f 100644 --- a/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/UncorrelatedFaultInjector.kt +++ b/simulator/opendc/opendc-core/src/main/kotlin/com/atlarge/opendc/core/failure/UncorrelatedFaultInjector.kt @@ -25,11 +25,11 @@ package com.atlarge.opendc.core.failure import com.atlarge.odcsim.simulationContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlin.math.ln1p import kotlin.math.pow import kotlin.random.Random +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * A [FaultInjector] that injects uncorrelated faults into the system, meaning that failures of the subsystems are diff --git a/simulator/opendc/opendc-experiments-sc18/src/main/kotlin/com/atlarge/opendc/experiments/sc18/TestExperiment.kt b/simulator/opendc/opendc-experiments-sc18/src/main/kotlin/com/atlarge/opendc/experiments/sc18/TestExperiment.kt index b0182ab3..7659b18e 100644 --- a/simulator/opendc/opendc-experiments-sc18/src/main/kotlin/com/atlarge/opendc/experiments/sc18/TestExperiment.kt +++ b/simulator/opendc/opendc-experiments-sc18/src/main/kotlin/com/atlarge/opendc/experiments/sc18/TestExperiment.kt @@ -38,6 +38,8 @@ import com.atlarge.opendc.workflows.service.stage.resource.FirstFitResourceSelec import com.atlarge.opendc.workflows.service.stage.resource.FunctionalResourceFilterPolicy import com.atlarge.opendc.workflows.service.stage.task.NullTaskEligibilityPolicy import com.atlarge.opendc.workflows.service.stage.task.SubmissionTimeTaskOrderPolicy +import java.io.File +import java.util.ServiceLoader import kotlin.math.max import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -46,8 +48,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.io.File -import java.util.ServiceLoader /** * Main entry point of the experiment. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/Main.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/Main.kt index 677af381..faa68e34 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/Main.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/Main.kt @@ -48,9 +48,9 @@ import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.file import com.github.ajalt.clikt.parameters.types.int -import mu.KotlinLogging import java.io.File import java.io.InputStream +import mu.KotlinLogging /** * The logger for this experiment. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/ExperimentHelpers.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/ExperimentHelpers.kt index a70297d2..b09c0dbb 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/ExperimentHelpers.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/ExperimentHelpers.kt @@ -45,19 +45,20 @@ import com.atlarge.opendc.experiments.sc20.experiment.monitor.ExperimentMonitor import com.atlarge.opendc.experiments.sc20.trace.Sc20StreamingParquetTraceReader import com.atlarge.opendc.format.environment.EnvironmentReader import com.atlarge.opendc.format.trace.TraceReader +import java.io.File +import kotlin.math.ln +import kotlin.math.max +import kotlin.random.Random import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mu.KotlinLogging -import java.io.File -import kotlin.math.ln -import kotlin.math.max -import kotlin.random.Random /** * The logger for this experiment. @@ -209,7 +210,6 @@ suspend fun processTrace(reader: TraceReader, scheduler: SimpleVirtP try { var submitted = 0 - val finished = Channel(Channel.CONFLATED) while (reader.hasNext()) { val (time, workload) = reader.next() @@ -228,17 +228,20 @@ suspend fun processTrace(reader: TraceReader, scheduler: SimpleVirtP if (it is ServerEvent.StateChanged) { monitor.reportVmStateChange(simulationContext.clock.millis(), it.server) } - - delay(1) - finished.send(Unit) } .collect() } } - while (scheduler.finishedVms + scheduler.unscheduledVms != submitted) { - finished.receive() - } + scheduler.events + .takeWhile { + when (it) { + is VirtProvisioningEvent.MetricsAvailable -> + it.inactiveVmCount + it.failedVmCount != submitted + } + } + .collect() + delay(1) } finally { reader.close() } diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/Run.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/Run.kt index 5d1c29e2..1580e4dd 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/Run.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/Run.kt @@ -38,13 +38,13 @@ import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionC import com.atlarge.opendc.experiments.sc20.trace.Sc20ParquetTraceReader import com.atlarge.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader import com.atlarge.opendc.format.environment.sc20.Sc20ClusterEnvironmentReader +import java.io.File +import java.util.ServiceLoader +import kotlin.random.Random import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import mu.KotlinLogging -import java.io.File -import java.util.ServiceLoader -import kotlin.random.Random /** * The logger for the experiment scenario. @@ -106,7 +106,11 @@ public data class Run(override val parent: Scenario, val id: Int, val seed: Int) ?.construct(seeder) ?: emptyMap() val trace = Sc20ParquetTraceReader(rawReaders, performanceInterferenceModel, parent.workload, seed) - val monitor = ParquetExperimentMonitor(this) + val monitor = ParquetExperimentMonitor( + parent.parent.parent.output, + "portfolio_id=${parent.parent.id}/scenario_id=${parent.id}/run_id=$id", + parent.parent.parent.bufferSize + ) root.launch { val (bareMetalProvisioner, scheduler) = createProvisioner( diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/monitor/ParquetExperimentMonitor.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/monitor/ParquetExperimentMonitor.kt index be60e5b7..b931fef9 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/monitor/ParquetExperimentMonitor.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/experiment/monitor/ParquetExperimentMonitor.kt @@ -27,13 +27,12 @@ package com.atlarge.opendc.experiments.sc20.experiment.monitor import com.atlarge.opendc.compute.core.Server import com.atlarge.opendc.compute.virt.driver.VirtDriver import com.atlarge.opendc.compute.virt.service.VirtProvisioningEvent -import com.atlarge.opendc.experiments.sc20.experiment.Run import com.atlarge.opendc.experiments.sc20.telemetry.HostEvent import com.atlarge.opendc.experiments.sc20.telemetry.ProvisionerEvent import com.atlarge.opendc.experiments.sc20.telemetry.parquet.ParquetHostEventWriter import com.atlarge.opendc.experiments.sc20.telemetry.parquet.ParquetProvisionerEventWriter -import mu.KotlinLogging import java.io.File +import mu.KotlinLogging /** * The logger instance to use. @@ -43,15 +42,14 @@ private val logger = KotlinLogging.logger {} /** * An [ExperimentMonitor] that logs the events to a Parquet file. */ -class ParquetExperimentMonitor(val run: Run) : ExperimentMonitor { - private val partition = "portfolio_id=${run.parent.parent.id}/scenario_id=${run.parent.id}/run_id=${run.id}" +class ParquetExperimentMonitor(base: File, partition: String, bufferSize: Int) : ExperimentMonitor { private val hostWriter = ParquetHostEventWriter( - File(run.parent.parent.parent.output, "host-metrics/$partition/data.parquet"), - run.parent.parent.parent.bufferSize + File(base, "host-metrics/$partition/data.parquet"), + bufferSize ) private val provisionerWriter = ParquetProvisionerEventWriter( - File(run.parent.parent.parent.output, "provisioner-metrics/$partition/data.parquet"), - run.parent.parent.parent.bufferSize + File(base, "provisioner-metrics/$partition/data.parquet"), + bufferSize ) private val currentHostEvent = mutableMapOf() private var startTime = -1L diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt index 31632b8c..a7c8ba4d 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt @@ -25,12 +25,12 @@ package com.atlarge.opendc.experiments.sc20.runner.execution import com.atlarge.opendc.experiments.sc20.runner.ExperimentDescriptor +import java.util.concurrent.Executors import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.withContext -import java.util.concurrent.Executors /** * An [ExperimentScheduler] that runs experiments using a local thread pool. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt index 3b80276f..28a19172 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt @@ -30,8 +30,8 @@ import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionC import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionListener import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionResult import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentScheduler -import kotlinx.coroutines.runBlocking import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.runBlocking /** * The default implementation of the [ExperimentRunner] interface. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetEventWriter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetEventWriter.kt index a69bd4b2..e42ac654 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetEventWriter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetEventWriter.kt @@ -25,17 +25,17 @@ package com.atlarge.opendc.experiments.sc20.telemetry.parquet import com.atlarge.opendc.experiments.sc20.telemetry.Event +import java.io.Closeable +import java.io.File +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.BlockingQueue +import kotlin.concurrent.thread import mu.KotlinLogging import org.apache.avro.Schema import org.apache.avro.generic.GenericData import org.apache.hadoop.fs.Path import org.apache.parquet.avro.AvroParquetWriter import org.apache.parquet.hadoop.metadata.CompressionCodecName -import java.io.Closeable -import java.io.File -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.BlockingQueue -import kotlin.concurrent.thread /** * The logging instance to use. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetHostEventWriter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetHostEventWriter.kt index 3bc09435..9fa4e0fb 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetHostEventWriter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetHostEventWriter.kt @@ -25,10 +25,10 @@ package com.atlarge.opendc.experiments.sc20.telemetry.parquet import com.atlarge.opendc.experiments.sc20.telemetry.HostEvent +import java.io.File import org.apache.avro.Schema import org.apache.avro.SchemaBuilder import org.apache.avro.generic.GenericData -import java.io.File /** * A Parquet event writer for [HostEvent]s. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetProvisionerEventWriter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetProvisionerEventWriter.kt index 1f3b0472..3d28860c 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetProvisionerEventWriter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetProvisionerEventWriter.kt @@ -25,10 +25,10 @@ package com.atlarge.opendc.experiments.sc20.telemetry.parquet import com.atlarge.opendc.experiments.sc20.telemetry.ProvisionerEvent +import java.io.File import org.apache.avro.Schema import org.apache.avro.SchemaBuilder import org.apache.avro.generic.GenericData -import java.io.File /** * A Parquet event writer for [ProvisionerEvent]s. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt index 1549b8d2..c1724369 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt @@ -25,10 +25,10 @@ package com.atlarge.opendc.experiments.sc20.telemetry.parquet import com.atlarge.opendc.experiments.sc20.telemetry.RunEvent +import java.io.File import org.apache.avro.Schema import org.apache.avro.SchemaBuilder import org.apache.avro.generic.GenericData -import java.io.File /** * A Parquet event writer for [RunEvent]s. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt index 652f7746..f9709b9f 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt @@ -30,12 +30,12 @@ import com.atlarge.opendc.compute.core.workload.VmWorkload import com.atlarge.opendc.core.User import com.atlarge.opendc.format.trace.TraceEntry import com.atlarge.opendc.format.trace.TraceReader +import java.io.File +import java.util.UUID import mu.KotlinLogging import org.apache.avro.generic.GenericData import org.apache.hadoop.fs.Path import org.apache.parquet.avro.AvroParquetReader -import java.io.File -import java.util.UUID private val logger = KotlinLogging.logger {} diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt index f6d6e6fd..8b7b222f 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt @@ -32,6 +32,14 @@ import com.atlarge.opendc.compute.core.workload.VmWorkload import com.atlarge.opendc.core.User import com.atlarge.opendc.format.trace.TraceEntry import com.atlarge.opendc.format.trace.TraceReader +import java.io.File +import java.io.Serializable +import java.util.SortedSet +import java.util.TreeSet +import java.util.UUID +import java.util.concurrent.ArrayBlockingQueue +import kotlin.concurrent.thread +import kotlin.random.Random import mu.KotlinLogging import org.apache.avro.generic.GenericData import org.apache.hadoop.fs.Path @@ -41,14 +49,6 @@ import org.apache.parquet.filter2.predicate.FilterApi import org.apache.parquet.filter2.predicate.Statistics import org.apache.parquet.filter2.predicate.UserDefinedPredicate import org.apache.parquet.io.api.Binary -import java.io.File -import java.io.Serializable -import java.util.SortedSet -import java.util.TreeSet -import java.util.UUID -import java.util.concurrent.ArrayBlockingQueue -import kotlin.concurrent.thread -import kotlin.random.Random private val logger = KotlinLogging.logger {} diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20TraceConverter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20TraceConverter.kt index 0877ad52..d6726910 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20TraceConverter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/Sc20TraceConverter.kt @@ -25,6 +25,12 @@ package com.atlarge.opendc.experiments.sc20.trace import com.atlarge.opendc.format.trace.sc20.Sc20VmPlacementReader +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import java.util.Random +import kotlin.math.max +import kotlin.math.min import me.tongfei.progressbar.ProgressBar import org.apache.avro.Schema import org.apache.avro.SchemaBuilder @@ -33,12 +39,6 @@ import org.apache.hadoop.fs.Path import org.apache.parquet.avro.AvroParquetWriter import org.apache.parquet.hadoop.ParquetWriter import org.apache.parquet.hadoop.metadata.CompressionCodecName -import java.io.BufferedReader -import java.io.File -import java.io.FileReader -import java.util.Random -import kotlin.math.max -import kotlin.math.min /** * A script to convert a trace in text format into a Parquet trace. diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/WorkloadSampler.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/WorkloadSampler.kt index dd70d4f1..f2a0e627 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/WorkloadSampler.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/trace/WorkloadSampler.kt @@ -28,8 +28,8 @@ import com.atlarge.opendc.compute.core.workload.VmWorkload import com.atlarge.opendc.experiments.sc20.experiment.model.CompositeWorkload import com.atlarge.opendc.experiments.sc20.experiment.model.Workload import com.atlarge.opendc.format.trace.TraceEntry -import mu.KotlinLogging import kotlin.random.Random +import mu.KotlinLogging private val logger = KotlinLogging.logger {} diff --git a/simulator/opendc/opendc-experiments-sc20/src/test/kotlin/com/atlarge/opendc/experiments/sc20/Sc20IntegrationTest.kt b/simulator/opendc/opendc-experiments-sc20/src/test/kotlin/com/atlarge/opendc/experiments/sc20/Sc20IntegrationTest.kt index 5ecf7605..a79e9a5a 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/test/kotlin/com/atlarge/opendc/experiments/sc20/Sc20IntegrationTest.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/test/kotlin/com/atlarge/opendc/experiments/sc20/Sc20IntegrationTest.kt @@ -42,6 +42,8 @@ import com.atlarge.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader import com.atlarge.opendc.format.environment.EnvironmentReader import com.atlarge.opendc.format.environment.sc20.Sc20ClusterEnvironmentReader import com.atlarge.opendc.format.trace.TraceReader +import java.io.File +import java.util.ServiceLoader import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch @@ -52,8 +54,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll -import java.io.File -import java.util.ServiceLoader /** * An integration test suite for the SC20 experiments. diff --git a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/environment/sc18/Sc18EnvironmentReader.kt b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/environment/sc18/Sc18EnvironmentReader.kt index 5f220ad0..a9aa3337 100644 --- a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/environment/sc18/Sc18EnvironmentReader.kt +++ b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/environment/sc18/Sc18EnvironmentReader.kt @@ -25,9 +25,9 @@ package com.atlarge.opendc.format.environment.sc18 import com.atlarge.odcsim.Domain -import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.core.MemoryUnit import com.atlarge.opendc.compute.core.ProcessingNode +import com.atlarge.opendc.compute.core.ProcessingUnit import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver import com.atlarge.opendc.compute.metal.service.ProvisioningService import com.atlarge.opendc.compute.metal.service.SimpleProvisioningService diff --git a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt index 2a8fefeb..1cabc8bc 100644 --- a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt +++ b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt @@ -26,10 +26,10 @@ package com.atlarge.opendc.format.trace.bitbrains import com.atlarge.opendc.compute.core.image.FlopsHistoryFragment import com.atlarge.opendc.compute.core.image.VmImage -import com.atlarge.opendc.compute.core.workload.VmWorkload -import com.atlarge.opendc.core.User import com.atlarge.opendc.compute.core.workload.IMAGE_PERF_INTERFERENCE_MODEL import com.atlarge.opendc.compute.core.workload.PerformanceInterferenceModel +import com.atlarge.opendc.compute.core.workload.VmWorkload +import com.atlarge.opendc.core.User import com.atlarge.opendc.format.trace.TraceEntry import com.atlarge.opendc.format.trace.TraceReader import java.io.BufferedReader diff --git a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/sc20/Sc20TraceReader.kt b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/sc20/Sc20TraceReader.kt index 076274d5..8e34505a 100644 --- a/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/sc20/Sc20TraceReader.kt +++ b/simulator/opendc/opendc-format/src/main/kotlin/com/atlarge/opendc/format/trace/sc20/Sc20TraceReader.kt @@ -26,10 +26,10 @@ package com.atlarge.opendc.format.trace.sc20 import com.atlarge.opendc.compute.core.image.FlopsHistoryFragment import com.atlarge.opendc.compute.core.image.VmImage -import com.atlarge.opendc.compute.core.workload.VmWorkload -import com.atlarge.opendc.core.User import com.atlarge.opendc.compute.core.workload.IMAGE_PERF_INTERFERENCE_MODEL import com.atlarge.opendc.compute.core.workload.PerformanceInterferenceModel +import com.atlarge.opendc.compute.core.workload.VmWorkload +import com.atlarge.opendc.core.User import com.atlarge.opendc.format.trace.TraceEntry import com.atlarge.opendc.format.trace.TraceReader import java.io.BufferedReader diff --git a/simulator/opendc/opendc-format/src/test/kotlin/com/atlarge/opendc/format/trace/swf/SwfTraceReaderTest.kt b/simulator/opendc/opendc-format/src/test/kotlin/com/atlarge/opendc/format/trace/swf/SwfTraceReaderTest.kt index 41ad8aba..94e4b0fc 100644 --- a/simulator/opendc/opendc-format/src/test/kotlin/com/atlarge/opendc/format/trace/swf/SwfTraceReaderTest.kt +++ b/simulator/opendc/opendc-format/src/test/kotlin/com/atlarge/opendc/format/trace/swf/SwfTraceReaderTest.kt @@ -1,8 +1,8 @@ package com.atlarge.opendc.format.trace.swf +import java.io.File import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import java.io.File class SwfTraceReaderTest { @Test diff --git a/simulator/opendc/opendc-runner-web/build.gradle.kts b/simulator/opendc/opendc-runner-web/build.gradle.kts index 50789789..52a59694 100644 --- a/simulator/opendc/opendc-runner-web/build.gradle.kts +++ b/simulator/opendc/opendc-runner-web/build.gradle.kts @@ -36,10 +36,13 @@ application { dependencies { api(project(":opendc:opendc-core")) + implementation(project(":opendc:opendc-compute")) + implementation(project(":opendc:opendc-format")) + implementation(project(":opendc:opendc-experiments-sc20")) implementation("com.github.ajalt:clikt:2.8.0") implementation("io.github.microutils:kotlin-logging:1.7.10") - implementation("org.mongodb:mongo-java-driver:3.12.6") + implementation("org.mongodb:mongodb-driver-sync:4.0.5") runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1") runtimeOnly(project(":odcsim:odcsim-engine-omega")) diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt index 3cee259c..d70ad6bd 100644 --- a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt @@ -1,16 +1,299 @@ package com.atlarge.opendc.runner.web +import com.atlarge.odcsim.SimulationEngineProvider +import com.atlarge.opendc.compute.virt.service.allocation.* +import com.atlarge.opendc.experiments.sc20.experiment.attachMonitor +import com.atlarge.opendc.experiments.sc20.experiment.createFailureDomain +import com.atlarge.opendc.experiments.sc20.experiment.createProvisioner +import com.atlarge.opendc.experiments.sc20.experiment.model.Workload +import com.atlarge.opendc.experiments.sc20.experiment.monitor.ParquetExperimentMonitor +import com.atlarge.opendc.experiments.sc20.experiment.processTrace +import com.atlarge.opendc.experiments.sc20.trace.Sc20ParquetTraceReader +import com.atlarge.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.clikt.parameters.types.int +import com.mongodb.MongoClientSettings +import com.mongodb.MongoCredential +import com.mongodb.ServerAddress +import com.mongodb.client.MongoClients +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import com.mongodb.client.model.Filters +import java.io.File +import java.util.* +import kotlin.random.Random +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import mu.KotlinLogging +import org.bson.Document private val logger = KotlinLogging.logger {} +/** + * The provider for the simulation engine to use. + */ +private val provider = ServiceLoader.load(SimulationEngineProvider::class.java).first() + /** * Represents the CLI command for starting the OpenDC web runner. */ class RunnerCli : CliktCommand(name = "runner") { - override fun run() { + /** + * The name of the database to use. + */ + private val mongoDb by option( + "--mongo-db", + help = "name of the database to use", + envvar = "OPENDC_DB" + ) + .default("opendc") + + /** + * The database host to connect to. + */ + private val mongoHost by option( + "--mongo-host", + help = "database host to connect to", + envvar = "OPENDC_DB_HOST" + ) + .default("localhost") + + /** + * The database port to connect to. + */ + private val mongoPort by option( + "--mongo-port", + help = "database port to connect to", + envvar = "OPENDC_DB_PORT" + ) + .int() + .default(27017) + + /** + * The database user to connect with. + */ + private val mongoUser by option( + "--mongo-user", + help = "database user to connect with", + envvar = "OPENDC_DB_USER" + ) + .default("opendc") + + /** + * The database password to connect with. + */ + private val mongoPassword by option( + "--mongo-password", + help = "database password to connect with", + envvar = "OPENDC_DB_PASSWORD" + ) + .convert { it.toCharArray() } + .required() + + /** + * The path to the traces directory. + */ + private val tracePath by option( + "--traces", + help = "path to the directory containing the traces", + envvar = "OPENDC_TRACES" + ) + .file(canBeFile = false) + .defaultLazy { File("traces/") } + + /** + * The path to the output directory. + */ + private val outputPath by option( + "--output", + help = "path to the results directory" + ) + .file(canBeFile = false) + .defaultLazy { File("results/") } + + /** + * Connect to the user-specified database. + */ + private fun createDatabase(): MongoDatabase { + val credential = MongoCredential.createScramSha1Credential( + mongoUser, + mongoDb, + mongoPassword + ) + + val settings = MongoClientSettings.builder() + .credential(credential) + .applyToClusterSettings { it.hosts(listOf(ServerAddress(mongoHost, mongoPort))) } + .build() + val client = MongoClients.create(settings) + return client.getDatabase(mongoDb) + } + + /** + * Run a single scenario. + */ + private suspend fun runScenario(portfolio: Document, scenario: Document, topologies: MongoCollection) { + val id = scenario.getString("_id") + val traceReader = Sc20RawParquetTraceReader( + File( + tracePath, + scenario.getEmbedded(listOf("trace", "traceId"), String::class.java) + ) + ) + + val targets = portfolio.get("targets", Document::class.java) + + repeat(targets.getInteger("repeatsPerScenario")) { + logger.info { "Starting repeat $it" } + runRepeat(scenario, it, topologies, traceReader) + } + + logger.info { "Finished scenario $id" } + } + + /** + * Run a single repeat. + */ + private suspend fun runRepeat( + scenario: Document, + repeat: Int, + topologies: MongoCollection, + traceReader: Sc20RawParquetTraceReader + ) { + val id = scenario.getString("_id") + val seed = repeat + val traceDocument = scenario.get("trace", Document::class.java) + val workloadName = traceDocument.getString("traceId") + val workloadFraction = traceDocument.get("loadSamplingFraction", Number::class.java).toDouble() + + val seeder = Random(seed) + val system = provider("experiment-$id") + val root = system.newDomain("root") + + val chan = Channel(Channel.CONFLATED) + + val operational = scenario.get("operational", Document::class.java) + val allocationPolicy = + when (val policyName = operational.getString("schedulerName")) { + "mem" -> AvailableMemoryAllocationPolicy() + "mem-inv" -> AvailableMemoryAllocationPolicy(true) + "core-mem" -> AvailableCoreMemoryAllocationPolicy() + "core-mem-inv" -> AvailableCoreMemoryAllocationPolicy(true) + "active-servers" -> NumberOfActiveServersAllocationPolicy() + "active-servers-inv" -> NumberOfActiveServersAllocationPolicy(true) + "provisioned-cores" -> ProvisionedCoresAllocationPolicy() + "provisioned-cores-inv" -> ProvisionedCoresAllocationPolicy(true) + "random" -> RandomAllocationPolicy(Random(seeder.nextInt())) + else -> throw IllegalArgumentException("Unknown policy $policyName") + } + + val trace = Sc20ParquetTraceReader( + listOf(traceReader), + emptyMap(), + Workload(workloadName, workloadFraction), + seed + ) + val topologyId = scenario.getEmbedded(listOf("topology", "topologyId"), String::class.java) + val environment = TopologyParser(topologies, topologyId) + val monitor = ParquetExperimentMonitor( + outputPath, + "scenario_id=$id/run_id=$repeat", + 4096 + ) + + root.launch { + val (bareMetalProvisioner, scheduler) = createProvisioner( + root, + environment, + allocationPolicy + ) + + val failureDomain = if (operational.getBoolean("failuresEnabled")) { + logger.debug("ENABLING failures") + createFailureDomain( + seeder.nextInt(), + operational.getDouble("failureFrequency"), + bareMetalProvisioner, + chan + ) + } else { + null + } + + attachMonitor(scheduler, monitor) + processTrace( + trace, + scheduler, + chan, + monitor, + emptyMap() + ) + + logger.debug("SUBMIT=${scheduler.submittedVms}") + logger.debug("FAIL=${scheduler.unscheduledVms}") + logger.debug("QUEUED=${scheduler.queuedVms}") + logger.debug("RUNNING=${scheduler.runningVms}") + logger.debug("FINISHED=${scheduler.finishedVms}") + + failureDomain?.cancel() + scheduler.terminate() + } + + try { + system.run() + } finally { + system.terminate() + monitor.close() + } + } + + override fun run() = runBlocking(Dispatchers.Default) { logger.info { "Starting OpenDC web runner" } + + logger.info { "Connecting to MongoDB instance" } + val database = createDatabase() + val manager = ScenarioManager(database.getCollection("scenarios")) + val portfolios = database.getCollection("portfolios") + val topologies = database.getCollection("topologies") + + logger.info { "Watching for queued scenarios" } + + while (true) { + val scenario = manager.findNext() + + if (scenario == null) { + delay(5000) + continue + } + + val id = scenario.getString("_id") + + logger.info { "Found queued scenario $id: attempting to claim" } + + if (!manager.claim(id)) { + logger.info { "Failed to claim scenario" } + continue + } + + coroutineScope { + // Launch heartbeat process + launch { + delay(60000) + manager.heartbeat(id) + } + + try { + val portfolio = portfolios.find(Filters.eq("_id", scenario.getString("portfolioId"))).first()!! + runScenario(portfolio, scenario, topologies) + manager.finish(id) + } catch (e: Exception) { + logger.warn(e) { "Scenario failed to finish" } + manager.fail(id) + } + } + } } } diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt new file mode 100644 index 00000000..0f375385 --- /dev/null +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt @@ -0,0 +1,77 @@ +package com.atlarge.opendc.runner.web + +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import java.time.Instant +import org.bson.Document + +/** + * Manages the queue of scenarios that need to be processed. + */ +class ScenarioManager(private val collection: MongoCollection) { + /** + * Find the next scenario that the simulator needs to process. + */ + fun findNext(): Document? { + return collection + .find(Filters.eq("simulation.state", "QUEUED")) + .first() + } + + /** + * Claim the scenario in the database with the specified id. + */ + fun claim(id: String): Boolean { + val res = collection.findOneAndUpdate( + Filters.and( + Filters.eq("_id", id), + Filters.eq("simulation.state", "QUEUED") + ), + Updates.combine( + Updates.set("simulation.state", "RUNNING"), + Updates.set("simulation.time", Instant.now()) + ) + ) + return res != null + } + + /** + * Update the heartbeat of the specified scenario. + */ + fun heartbeat(id: String) { + collection.findOneAndUpdate( + Filters.and( + Filters.eq("_id", id), + Filters.eq("simulation.state", "RUNNING") + ), + Updates.set("simulation.time", Instant.now()) + ) + } + + /** + * Mark the scenario as failed. + */ + fun fail(id: String) { + collection.findOneAndUpdate( + Filters.and( + Filters.eq("_id", id), + Filters.eq("simulation.state", "FAILED") + ), + Updates.set("simulation.time", Instant.now()) + ) + } + + /** + * Mark the scenario as finished. + */ + fun finish(id: String) { + collection.findOneAndUpdate( + Filters.and( + Filters.eq("_id", id), + Filters.eq("simulation.state", "FINISHED") + ), + Updates.set("simulation.time", Instant.now()) + ) + } +} diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/TopologyParser.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/TopologyParser.kt new file mode 100644 index 00000000..499585ec --- /dev/null +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/TopologyParser.kt @@ -0,0 +1,127 @@ +package com.atlarge.opendc.runner.web + +import com.atlarge.odcsim.Domain +import com.atlarge.opendc.compute.core.MemoryUnit +import com.atlarge.opendc.compute.core.ProcessingNode +import com.atlarge.opendc.compute.core.ProcessingUnit +import com.atlarge.opendc.compute.metal.NODE_CLUSTER +import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver +import com.atlarge.opendc.compute.metal.power.LinearLoadPowerModel +import com.atlarge.opendc.compute.metal.service.ProvisioningService +import com.atlarge.opendc.compute.metal.service.SimpleProvisioningService +import com.atlarge.opendc.core.Environment +import com.atlarge.opendc.core.Platform +import com.atlarge.opendc.core.Zone +import com.atlarge.opendc.core.services.ServiceRegistry +import com.atlarge.opendc.format.environment.EnvironmentReader +import com.mongodb.client.AggregateIterable +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.Aggregates +import com.mongodb.client.model.Field +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Projections +import java.util.* +import kotlinx.coroutines.launch +import org.bson.Document + +/** + * A helper class that converts the MongoDB topology into an OpenDC environment. + */ +class TopologyParser(private val collection: MongoCollection, private val id: String) : EnvironmentReader { + /** + * Parse the topology with the specified [id]. + */ + override suspend fun construct(dom: Domain): Environment { + val nodes = mutableListOf() + val random = Random(0) + + for (machine in fetchMachines(id)) { + val machineId = machine.getString("_id") + val clusterId = machine.getString("rack_id") + val position = machine.getInteger("position") + + val processors = machine.getList("cpus", Document::class.java).flatMap { cpu -> + val cores = cpu.getInteger("numberOfCores") + val speed = cpu.get("clockRateMhz", Number::class.java).toDouble() + // TODO Remove hardcoding of vendor + val node = ProcessingNode("Intel", "amd64", cpu.getString("name"), cores) + List(cores) { coreId -> + ProcessingUnit(node, coreId, speed) + } + } + val memoryUnits = machine.getList("memories", Document::class.java).map { memory -> + MemoryUnit( + "Samsung", + memory.getString("name"), + memory.get("speedMbPerS", Number::class.java).toDouble(), + memory.get("sizeMb", Number::class.java).toLong() + ) + } + nodes.add( + SimpleBareMetalDriver( + dom.newDomain(machineId), + UUID(random.nextLong(), random.nextLong()), + "node-$clusterId-$position", + mapOf(NODE_CLUSTER to clusterId), + processors, + memoryUnits, + // For now we assume a simple linear load model with an idle draw of ~200W and a maximum + // power draw of 350W. + // Source: https://stackoverflow.com/questions/6128960 + LinearLoadPowerModel(200.0, 350.0) + ) + ) + } + + val provisioningService = SimpleProvisioningService(dom.newDomain("provisioner")) + dom.launch { + for (node in nodes) { + provisioningService.create(node) + } + } + + val serviceRegistry = ServiceRegistry().put(ProvisioningService, provisioningService) + + val platform = Platform( + UUID.randomUUID(), "opendc-platform", listOf( + Zone(UUID.randomUUID(), "zone", serviceRegistry) + ) + ) + + return Environment(fetchName(id), null, listOf(platform)) + } + + override fun close() {} + + /** + * Fetch the metadata of the topology. + */ + private fun fetchName(id: String): String { + return collection.aggregate( + listOf( + Aggregates.match(Filters.eq("_id", id)), + Aggregates.project(Projections.include("name")) + ) + ) + .first()!! + .getString("name") + } + + /** + * Fetch a topology from the database with the specified [id]. + */ + private fun fetchMachines(id: String): AggregateIterable { + return collection.aggregate( + listOf( + Aggregates.match(Filters.eq("_id", id)), + Aggregates.project(Projections.fields(Document("racks", "\$rooms.tiles.rack"))), + Aggregates.unwind("\$racks"), + Aggregates.unwind("\$racks"), + Aggregates.replaceRoot("\$racks"), + Aggregates.addFields(Field("machines.rack_id", "\$_id")), + Aggregates.unwind("\$machines"), + Aggregates.replaceRoot("\$machines") + ) + ) + } +} diff --git a/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowService.kt b/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowService.kt index 7c7990e2..1193f7b2 100644 --- a/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowService.kt +++ b/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowService.kt @@ -39,13 +39,13 @@ import com.atlarge.opendc.workflows.service.stage.resource.ResourceSelectionPoli import com.atlarge.opendc.workflows.service.stage.task.TaskEligibilityPolicy import com.atlarge.opendc.workflows.service.stage.task.TaskOrderPolicy import com.atlarge.opendc.workflows.workload.Job +import java.util.PriorityQueue +import java.util.Queue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import java.util.PriorityQueue -import java.util.Queue import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/WorkflowService.kt b/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/WorkflowService.kt index ad818dde..a60ba0e2 100644 --- a/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/WorkflowService.kt +++ b/simulator/opendc/opendc-workflows/src/main/kotlin/com/atlarge/opendc/workflows/service/WorkflowService.kt @@ -26,8 +26,8 @@ package com.atlarge.opendc.workflows.service import com.atlarge.opendc.core.services.AbstractServiceKey import com.atlarge.opendc.workflows.workload.Job -import kotlinx.coroutines.flow.Flow import java.util.UUID +import kotlinx.coroutines.flow.Flow /** * A service for cloud workflow management. diff --git a/simulator/opendc/opendc-workflows/src/test/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt b/simulator/opendc/opendc-workflows/src/test/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt index 5ee6d5e6..5c129e37 100644 --- a/simulator/opendc/opendc-workflows/src/test/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt +++ b/simulator/opendc/opendc-workflows/src/test/kotlin/com/atlarge/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt @@ -35,6 +35,8 @@ import com.atlarge.opendc.workflows.service.stage.resource.FirstFitResourceSelec import com.atlarge.opendc.workflows.service.stage.resource.FunctionalResourceFilterPolicy import com.atlarge.opendc.workflows.service.stage.task.NullTaskEligibilityPolicy import com.atlarge.opendc.workflows.service.stage.task.SubmissionTimeTaskOrderPolicy +import java.util.ServiceLoader +import kotlin.math.max import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect @@ -44,8 +46,6 @@ import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import java.util.ServiceLoader -import kotlin.math.max /** * Integration test suite for the [StageWorkflowService]. -- cgit v1.2.3 From fc5405bab041545f4b7f04faa22fb21cc84f5c43 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 16 Jul 2020 22:30:57 +0200 Subject: Add docker-compose service for simulator This change re-adds the simulator service for the docker-compose configuration, such that it will listen for incoming jobs from the API. --- docker-compose.yml | 43 ++++++++++++++++++++++--------------------- simulator/Dockerfile | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 simulator/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 837f9019..5e45ea59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3.8" services: frontend: build: @@ -33,26 +33,26 @@ services: - OPENDC_ROOT_DIR - OPENDC_SERVER_BASE_URL - # TODO: Implement new database interaction on the simulator side - # simulator: - # build: - # context: ./opendc-simulator - # dockerfile: opendc-model-odc/setup/Dockerfile - # image: simulator - # restart: on-failure - # links: - # - mongo - # depends_on: - # - mongo - # environment: - # - PERSISTENCE_URL=jdbc:mysql://mariadb:3306/opendc - # - PERSISTENCE_USER=opendc - # - PERSISTENCE_PASSWORD=opendcpassword - # - COLLECT_MACHINE_STATES=ON - # - COLLECT_TASK_STATES=ON - # - COLLECT_STAGE_MEASUREMENTS=OFF - # - COLLECT_TASK_METRICS=OFF - # - COLLECT_JOB_METRICS=OFF + simulator: + build: ./simulator + image: simulator + restart: on-failure + networks: + - backend + depends_on: + - mongo + volumes: + - type: bind + source: ./traces + target: /home/gradle/simulator/traces + - type: volume + source: results-volume + target: /home/gradle/simulator/results + environment: + - OPENDC_DB + - OPENDC_DB_USERNAME + - OPENDC_DB_PASSWORD + - OPENDC_DB_HOST=mongo mongo: build: @@ -90,6 +90,7 @@ services: volumes: mongo-volume: external: false + results-volume: networks: backend: {} diff --git a/simulator/Dockerfile b/simulator/Dockerfile new file mode 100644 index 00000000..c923cddf --- /dev/null +++ b/simulator/Dockerfile @@ -0,0 +1,27 @@ +FROM gradle:jdk14 +MAINTAINER OpenDC Maintainers + +# Set the home directory to our gradle user's home. +ENV HOME=/home/gradle +ENV APP_HOME=$HOME/simulator + +# Copy OpenDC simulator +COPY ./ $APP_HOME + +# Build as root +USER root + +# Set the working directory to the simulator +WORKDIR $APP_HOME + +# Build the application +RUN gradle --no-daemon assemble installDist + +# Fix permissions +RUN chown -R gradle:gradle $APP_HOME + +# Downgrade user +USER gradle + +# Start the Gradle application on run +CMD opendc/opendc-runner-web/build/install/opendc-runner-web/bin/opendc-runner-web -- cgit v1.2.3 From 4fa2e0dd8e2148c715b2ce691ea471d5af9a9cdb Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Fri, 17 Jul 2020 10:07:42 +0200 Subject: Fix tests --- api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py index 09b7d0c0..c3be0215 100644 --- a/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py +++ b/api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py @@ -19,6 +19,7 @@ def test_get_scenario_not_authorized(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ + 'projectId': '1', 'portfolioId': '1', '_id': '1', 'authorizations': [{ @@ -34,6 +35,7 @@ def test_get_scenario(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ + 'projectId': '1', 'portfolioId': '1', '_id': '1', 'authorizations': [{ @@ -63,6 +65,7 @@ def test_update_scenario_not_authorized(client, mocker): 'fetch_one', return_value={ '_id': '1', + 'projectId': '1', 'portfolioId': '1', 'authorizations': [{ 'projectId': '1', @@ -82,6 +85,7 @@ def test_update_scenario(client, mocker): 'fetch_one', return_value={ '_id': '1', + 'projectId': '1', 'portfolioId': '1', 'authorizations': [{ 'projectId': '1', @@ -110,6 +114,7 @@ def test_delete_project_different_user(client, mocker): 'fetch_one', return_value={ '_id': '1', + 'projectId': '1', 'portfolioId': '1', 'googleId': 'other_test', 'authorizations': [{ @@ -126,6 +131,7 @@ def test_delete_project(client, mocker): 'fetch_one', return_value={ '_id': '1', + 'projectId': '1', 'portfolioId': '1', 'googleId': 'test', 'scenarioIds': ['1'], -- cgit v1.2.3 From 2d625732ed0d74f4291370bddc96df27219b78e6 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 17 Jul 2020 11:22:44 +0200 Subject: Fix conditional Github Actions trigger This change fixes the conditional Github Actions trigger that we use to only trigger CI invocations for the changed subprojects. Previously, Github Actions was only triggered when a file in the top-level directory of the subproject was changed. --- .github/workflows/api.yml | 54 ++++++++++++++++++------------------- .github/workflows/frontend.yml | 44 +++++++++++++++--------------- .github/workflows/simulator.yml | 60 ++++++++++++++++++++--------------------- 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 7335c737..ae67b753 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -1,34 +1,34 @@ name: REST API on: - push: - paths: - - 'api/*' + push: + paths: + - 'api/**' defaults: - run: - working-directory: api + run: + working-directory: api jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - python: [3.8] - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Lint with pylint - run: | - ./check.sh - - name: Test with pytest - run: | - pytest opendc + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python: [3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with pylint + run: | + ./check.sh + - name: Test with pytest + run: | + pytest opendc diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index ec4a7e71..da6f1031 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,29 +1,29 @@ name: Frontend on: - push: - paths: - - 'frontend/*' + push: + paths: + - 'frontend/**' defaults: - run: - working-directory: frontend + run: + working-directory: frontend jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - node: [12.x] - steps: - - uses: actions/checkout@v2 - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - run: npm install - - run: npm run build --if-present - - run: npm test - env: - CI: true + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node: [12.x] + steps: + - uses: actions/checkout@v2 + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run build --if-present + - run: npm test + env: + CI: true diff --git a/.github/workflows/simulator.yml b/.github/workflows/simulator.yml index 887d4af6..8174ae3a 100644 --- a/.github/workflows/simulator.yml +++ b/.github/workflows/simulator.yml @@ -1,37 +1,37 @@ name: Simulator on: - push: - paths: - - 'simulator/*' + push: + paths: + - 'simulator/**' defaults: - run: - working-directory: simulator + run: + working-directory: simulator jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - java: [14] - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Build with Gradle - run: ./gradlew assemble - - name: Check with Gradle - run: ./gradlew check --info + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + java: [14] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build with Gradle + run: ./gradlew assemble + - name: Check with Gradle + run: ./gradlew check --info -- cgit v1.2.3 From 4ea3f3aa9fd36717dc1c1ded81708c3fb893de9c Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 17 Jul 2020 11:24:14 +0200 Subject: Reformat API spec This change reformats the API spec to follow the YAML spec and the updated EditorConfig. --- opendc-api-spec.yml | 1790 +++++++++++++++++++++++++-------------------------- 1 file changed, 895 insertions(+), 895 deletions(-) diff --git a/opendc-api-spec.yml b/opendc-api-spec.yml index 36ef3c83..3009ab03 100644 --- a/opendc-api-spec.yml +++ b/opendc-api-spec.yml @@ -1,921 +1,921 @@ swagger: '2.0' info: - version: 1.0.0 - title: OpenDC API - description: 'OpenDC is an open-source datacenter simulator for education, featuring real-time online collaboration, diverse simulation models, and detailed performance feedback statistics.' + version: 1.0.0 + title: OpenDC API + description: 'OpenDC is an open-source datacenter simulator for education, featuring real-time online collaboration, diverse simulation models, and detailed performance feedback statistics.' host: opendc.org basePath: /v2 schemes: - - https + - https paths: - '/users': - get: - tags: - - users - description: Search for a User using their email address. - parameters: - - name: email - in: query - description: User's email address. - required: true - type: string - responses: - '200': - description: Successfully searched Users. - schema: - $ref: '#/definitions/User' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '404': - description: User not found. - post: - tags: - - users - description: Add a new User. - parameters: - - name: user - in: body - description: The new User. - required: true - schema: - $ref: '#/definitions/User' - responses: - '200': - description: Successfully added User. - schema: - $ref: '#/definitions/User' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '409': - description: User already exists. - '/users/{userId}': - get: - tags: - - users - description: Get this User. - parameters: - - name: userId - in: path - description: User's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved User. - schema: - $ref: '#/definitions/User' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '404': - description: User not found. - put: - tags: - - users - description: Update this User's given name and/ or family name. - parameters: - - name: userId - in: path - description: User's ID. - required: true - type: string - - name: user - in: body - description: User's new properties. - required: true - schema: - properties: - givenName: - type: string - familyName: - type: string - responses: - '200': - description: Successfully updated User. - schema: - $ref: '#/definitions/User' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating User. - '404': - description: User not found. - delete: - tags: - - users - description: Delete this User. - parameters: - - name: userId - in: path - description: User's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted User. - schema: - $ref: '#/definitions/User' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting User. - '404': - description: User not found. - '/projects': - post: - tags: - - projects - description: Add a Project. - parameters: - - name: project - in: body - description: The new Project. - required: true - schema: - properties: - name: - type: string - responses: - '200': - description: Successfully added Project. - schema: - $ref: '#/definitions/Project' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '/projects/{projectId}': - get: - tags: - - projects - description: Get this Project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Project. - schema: - $ref: '#/definitions/Project' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving Project. - '404': - description: Project not found - put: - tags: - - projects - description: Update this Project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - - name: project - in: body - description: Project's new properties. - required: true - schema: - properties: - project: - $ref: '#/definitions/Project' - responses: - '200': - description: Successfully updated Project. - schema: - $ref: '#/definitions/Project' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating Project. - '404': - description: Project not found. - delete: - tags: - - projects - description: Delete this project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted Project. - schema: - $ref: '#/definitions/Project' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting Project. - '404': - description: Project not found. - '/projects/{projectId}/authorizations': - get: - tags: - - projects - description: Get this Project's Authorizations. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Project's Authorizations. - schema: - type: array - items: - type: object - properties: - userId: - type: string - projectId: - type: string - authorizationLevel: - type: string - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving this Project's Authorizations. - '404': - description: Project not found. - '/projects/{projectId}/topologies': - post: - tags: - - projects - description: Add a Topology. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - - name: topology - in: body - description: The new Topology. - required: true - schema: - properties: - topology: - $ref: '#/definitions/Topology' - responses: - '200': - description: Successfully added Topology. - schema: - $ref: '#/definitions/Topology' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '/projects/{projectId}/portfolios': - post: - tags: - - portfolios - description: Add a Portfolio. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - type: string - - name: portfolio - in: body - description: The new Portfolio. - required: true - schema: - properties: - topology: - $ref: '#/definitions/Portfolio' - responses: - '200': - description: Successfully added Portfolio. - schema: - $ref: '#/definitions/Portfolio' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '/topologies/{topologyId}': - get: - tags: - - topologies - description: Get this Topology. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Topology. - schema: - $ref: '#/definitions/Topology' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving Topology. - '404': - description: Topology not found. - put: - tags: - - topologies - description: Update this Topology's name. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - type: string - - name: topology - in: body - description: Topology's new properties. - required: true - schema: - properties: - topology: - $ref: '#/definitions/Topology' - responses: - '200': - description: Successfully updated Topology. - schema: - $ref: '#/definitions/Topology' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating Topology. - '404': - description: Topology not found. - delete: - tags: - - topologies - description: Delete this Topology. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted Topology. - schema: - $ref: '#/definitions/Topology' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting Topology. - '404': - description: Topology not found. - '/portfolios/{portfolioId}': - get: - tags: - - portfolios - description: Get this Portfolio. - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Portfolio. - schema: - $ref: '#/definitions/Portfolio' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving Portfolio. - '404': - description: Portfolio not found. - put: - tags: - - portfolios - description: "Update this Portfolio." - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - type: string - - name: portfolio - in: body - description: Portfolio's new properties. - required: true - schema: - $ref: '#/definitions/Portfolio' - responses: - '200': - description: Successfully updated Portfolio. - schema: - $ref: '#/definitions/Portfolio' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating Portfolio. - '404': - description: 'Portfolio not found.' - delete: - tags: - - portfolios - description: Delete this Portfolio. - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted Portfolio. - schema: - $ref: '#/definitions/Portfolio' - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting Portfolio. - '404': - description: Portfolio not found. - '/scenarios/{scenarioId}': - get: - tags: - - scenarios - description: Get this Scenario. - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Scenario. - schema: - $ref: '#/definitions/Scenario' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving Scenario. - '404': - description: Scenario not found. - put: - tags: - - scenarios - description: "Update this Scenario's name (other properties are read-only)." - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - type: string - - name: scenario - in: body - description: Scenario with new name. - required: true - schema: - $ref: '#/definitions/Scenario' - responses: - '200': - description: Successfully updated Scenario. - schema: - $ref: '#/definitions/Scenario' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating Scenario. - '404': - description: 'Scenario not found.' - delete: - tags: - - scenarios - description: Delete this Scenario. - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted Scenario. - schema: - $ref: '#/definitions/Scenario' - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting Scenario. - '404': - description: Scenario not found. - /schedulers: - get: - tags: - - simulation - description: Get all available Schedulers - responses: - '200': - description: Successfully retrieved Schedulers. - schema: - type: array - items: - $ref: '#/definitions/Scheduler' - '401': - description: Unauthorized. - /traces: - get: - tags: - - simulation - description: Get all available Traces (non-populated). - responses: - '200': - description: Successfully retrieved Traces (non-populated). - schema: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - '401': - description: Unauthorized. - '/traces/{traceId}': - get: - tags: - - simulation - description: Get this Trace. - parameters: - - name: traceId - in: path - description: Trace's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Trace. - schema: - $ref: '#/definitions/Trace' - '401': - description: Unauthorized. - '404': - description: Trace not found. - /prefabs: - post: - tags: - - prefabs - description: Add a Prefab. - parameters: - - name: prefab - in: body - description: The new Prefab. - required: true - schema: - properties: - name: - type: string - responses: - '200': - description: Successfully added Prefab. - schema: - $ref: '#/definitions/Prefab' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '/prefabs/{prefabId}': - get: - tags: - - prefabs - description: Get this Prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - type: string - responses: - '200': - description: Successfully retrieved Prefab. - schema: - $ref: '#/definitions/Prefab' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from retrieving Prefab. - '404': - description: Prefab not found - put: - tags: - - prefabs - description: Update this Prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - type: string - - name: prefab - in: body - description: Prefab's new properties. - required: true - schema: - properties: - project: - $ref: '#/definitions/Prefab' - responses: - '200': - description: Successfully updated Prefab. - schema: - $ref: '#/definitions/Prefab' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from updating Prefab. - '404': - description: Prefab not found. - delete: - tags: - - prefabs - description: Delete this prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - type: string - responses: - '200': - description: Successfully deleted Prefab. - schema: - $ref: '#/definitions/Prefab' - '400': - description: Missing or incorrectly typed parameter. - '401': - description: Unauthorized. - '403': - description: Forbidden from deleting Prefab. - '404': - description: Prefab not found. - -definitions: - Prefab: - type: object - properties: - _id: + '/users': + get: + tags: + - users + description: Search for a User using their email address. + parameters: + - name: email + in: query + description: User's email address. + required: true + type: string + responses: + '200': + description: Successfully searched Users. + schema: + $ref: '#/definitions/User' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '404': + description: User not found. + post: + tags: + - users + description: Add a new User. + parameters: + - name: user + in: body + description: The new User. + required: true + schema: + $ref: '#/definitions/User' + responses: + '200': + description: Successfully added User. + schema: + $ref: '#/definitions/User' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '409': + description: User already exists. + '/users/{userId}': + get: + tags: + - users + description: Get this User. + parameters: + - name: userId + in: path + description: User's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved User. + schema: + $ref: '#/definitions/User' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '404': + description: User not found. + put: + tags: + - users + description: Update this User's given name and/ or family name. + parameters: + - name: userId + in: path + description: User's ID. + required: true + type: string + - name: user + in: body + description: User's new properties. + required: true + schema: + properties: + givenName: type: string - name: + familyName: type: string - datetimeCreated: + responses: + '200': + description: Successfully updated User. + schema: + $ref: '#/definitions/User' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating User. + '404': + description: User not found. + delete: + tags: + - users + description: Delete this User. + parameters: + - name: userId + in: path + description: User's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted User. + schema: + $ref: '#/definitions/User' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting User. + '404': + description: User not found. + '/projects': + post: + tags: + - projects + description: Add a Project. + parameters: + - name: project + in: body + description: The new Project. + required: true + schema: + properties: + name: type: string - format: dateTime - datetimeLastEdited: + responses: + '200': + description: Successfully added Project. + schema: + $ref: '#/definitions/Project' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '/projects/{projectId}': + get: + tags: + - projects + description: Get this Project. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Project. + schema: + $ref: '#/definitions/Project' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving Project. + '404': + description: Project not found + put: + tags: + - projects + description: Update this Project. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + - name: project + in: body + description: Project's new properties. + required: true + schema: + properties: + project: + $ref: '#/definitions/Project' + responses: + '200': + description: Successfully updated Project. + schema: + $ref: '#/definitions/Project' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating Project. + '404': + description: Project not found. + delete: + tags: + - projects + description: Delete this project. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted Project. + schema: + $ref: '#/definitions/Project' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting Project. + '404': + description: Project not found. + '/projects/{projectId}/authorizations': + get: + tags: + - projects + description: Get this Project's Authorizations. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Project's Authorizations. + schema: + type: array + items: + type: object + properties: + userId: + type: string + projectId: + type: string + authorizationLevel: + type: string + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving this Project's Authorizations. + '404': + description: Project not found. + '/projects/{projectId}/topologies': + post: + tags: + - projects + description: Add a Topology. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + - name: topology + in: body + description: The new Topology. + required: true + schema: + properties: + topology: + $ref: '#/definitions/Topology' + responses: + '200': + description: Successfully added Topology. + schema: + $ref: '#/definitions/Topology' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '/projects/{projectId}/portfolios': + post: + tags: + - portfolios + description: Add a Portfolio. + parameters: + - name: projectId + in: path + description: Project's ID. + required: true + type: string + - name: portfolio + in: body + description: The new Portfolio. + required: true + schema: + properties: + topology: + $ref: '#/definitions/Portfolio' + responses: + '200': + description: Successfully added Portfolio. + schema: + $ref: '#/definitions/Portfolio' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '/topologies/{topologyId}': + get: + tags: + - topologies + description: Get this Topology. + parameters: + - name: topologyId + in: path + description: Topology's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Topology. + schema: + $ref: '#/definitions/Topology' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving Topology. + '404': + description: Topology not found. + put: + tags: + - topologies + description: Update this Topology's name. + parameters: + - name: topologyId + in: path + description: Topology's ID. + required: true + type: string + - name: topology + in: body + description: Topology's new properties. + required: true + schema: + properties: + topology: + $ref: '#/definitions/Topology' + responses: + '200': + description: Successfully updated Topology. + schema: + $ref: '#/definitions/Topology' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating Topology. + '404': + description: Topology not found. + delete: + tags: + - topologies + description: Delete this Topology. + parameters: + - name: topologyId + in: path + description: Topology's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted Topology. + schema: + $ref: '#/definitions/Topology' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting Topology. + '404': + description: Topology not found. + '/portfolios/{portfolioId}': + get: + tags: + - portfolios + description: Get this Portfolio. + parameters: + - name: portfolioId + in: path + description: Portfolio's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Portfolio. + schema: + $ref: '#/definitions/Portfolio' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving Portfolio. + '404': + description: Portfolio not found. + put: + tags: + - portfolios + description: "Update this Portfolio." + parameters: + - name: portfolioId + in: path + description: Portfolio's ID. + required: true + type: string + - name: portfolio + in: body + description: Portfolio's new properties. + required: true + schema: + $ref: '#/definitions/Portfolio' + responses: + '200': + description: Successfully updated Portfolio. + schema: + $ref: '#/definitions/Portfolio' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating Portfolio. + '404': + description: 'Portfolio not found.' + delete: + tags: + - portfolios + description: Delete this Portfolio. + parameters: + - name: portfolioId + in: path + description: Portfolio's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted Portfolio. + schema: + $ref: '#/definitions/Portfolio' + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting Portfolio. + '404': + description: Portfolio not found. + '/scenarios/{scenarioId}': + get: + tags: + - scenarios + description: Get this Scenario. + parameters: + - name: scenarioId + in: path + description: Scenario's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Scenario. + schema: + $ref: '#/definitions/Scenario' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving Scenario. + '404': + description: Scenario not found. + put: + tags: + - scenarios + description: "Update this Scenario's name (other properties are read-only)." + parameters: + - name: scenarioId + in: path + description: Scenario's ID. + required: true + type: string + - name: scenario + in: body + description: Scenario with new name. + required: true + schema: + $ref: '#/definitions/Scenario' + responses: + '200': + description: Successfully updated Scenario. + schema: + $ref: '#/definitions/Scenario' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating Scenario. + '404': + description: 'Scenario not found.' + delete: + tags: + - scenarios + description: Delete this Scenario. + parameters: + - name: scenarioId + in: path + description: Scenario's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted Scenario. + schema: + $ref: '#/definitions/Scenario' + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting Scenario. + '404': + description: Scenario not found. + /schedulers: + get: + tags: + - simulation + description: Get all available Schedulers + responses: + '200': + description: Successfully retrieved Schedulers. + schema: + type: array + items: + $ref: '#/definitions/Scheduler' + '401': + description: Unauthorized. + /traces: + get: + tags: + - simulation + description: Get all available Traces (non-populated). + responses: + '200': + description: Successfully retrieved Traces (non-populated). + schema: + type: array + items: + type: object + properties: + _id: + type: string + name: + type: string + '401': + description: Unauthorized. + '/traces/{traceId}': + get: + tags: + - simulation + description: Get this Trace. + parameters: + - name: traceId + in: path + description: Trace's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Trace. + schema: + $ref: '#/definitions/Trace' + '401': + description: Unauthorized. + '404': + description: Trace not found. + /prefabs: + post: + tags: + - prefabs + description: Add a Prefab. + parameters: + - name: prefab + in: body + description: The new Prefab. + required: true + schema: + properties: + name: type: string - format: dateTime - Scheduler: - type: object - properties: - name: - type: string - Project: - type: object - properties: + responses: + '200': + description: Successfully added Prefab. + schema: + $ref: '#/definitions/Prefab' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '/prefabs/{prefabId}': + get: + tags: + - prefabs + description: Get this Prefab. + parameters: + - name: prefabId + in: path + description: Prefab's ID. + required: true + type: string + responses: + '200': + description: Successfully retrieved Prefab. + schema: + $ref: '#/definitions/Prefab' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from retrieving Prefab. + '404': + description: Prefab not found + put: + tags: + - prefabs + description: Update this Prefab. + parameters: + - name: prefabId + in: path + description: Prefab's ID. + required: true + type: string + - name: prefab + in: body + description: Prefab's new properties. + required: true + schema: + properties: + project: + $ref: '#/definitions/Prefab' + responses: + '200': + description: Successfully updated Prefab. + schema: + $ref: '#/definitions/Prefab' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from updating Prefab. + '404': + description: Prefab not found. + delete: + tags: + - prefabs + description: Delete this prefab. + parameters: + - name: prefabId + in: path + description: Prefab's ID. + required: true + type: string + responses: + '200': + description: Successfully deleted Prefab. + schema: + $ref: '#/definitions/Prefab' + '400': + description: Missing or incorrectly typed parameter. + '401': + description: Unauthorized. + '403': + description: Forbidden from deleting Prefab. + '404': + description: Prefab not found. + +definitions: + Prefab: + type: object + properties: + _id: + type: string + name: + type: string + datetimeCreated: + type: string + format: dateTime + datetimeLastEdited: + type: string + format: dateTime + Scheduler: + type: object + properties: + name: + type: string + Project: + type: object + properties: + _id: + type: string + name: + type: string + datetimeCreated: + type: string + format: dateTime + datetimeLastEdited: + type: string + format: dateTime + topologyIds: + type: array + items: + type: string + portfolioIds: + type: array + items: + type: string + Topology: + type: object + properties: + _id: + type: string + projectId: + type: string + name: + type: string + rooms: + type: array + items: + type: object + properties: _id: - type: string + type: string name: - type: string - datetimeCreated: - type: string - format: dateTime - datetimeLastEdited: - type: string - format: dateTime - topologyIds: - type: array - items: - type: string - portfolioIds: - type: array - items: + type: string + tiles: + type: array + items: + type: object + properties: + _id: type: string - Topology: - type: object - properties: - _id: - type: string - projectId: - type: string - name: - type: string - rooms: - type: array - items: + positionX: + type: integer + positionY: + type: integer + object: type: object properties: - _id: - type: string - name: - type: string - tiles: - type: array - items: + capacity: + type: integer + powerCapacityW: + type: integer + machines: + type: array + items: + type: object + properties: + position: + type: integer + cpuItems: + type: array + items: type: object properties: - _id: - type: string - positionX: - type: integer - positionY: - type: integer - object: - type: object - properties: - capacity: - type: integer - powerCapacityW: - type: integer - machines: - type: array - items: - type: object - properties: - position: - type: integer - cpuItems: - type: array - items: - type: object - properties: - name: - type: string - clockRateMhz: - type: integer - numberOfCores: - type: integer - gpuItems: - type: array - items: - type: object - properties: - name: - type: string - clockRateMhz: - type: integer - numberOfCores: - type: integer - memoryItems: - type: array - items: - type: object - properties: - name: - type: string - speedMbPerS: - type: integer - sizeMb: - type: integer - storageItems: - type: array - items: - type: integer - properties: - name: - type: string - speedMbPerS: - type: integer - sizeMb: - type: integer - Portfolio: + name: + type: string + clockRateMhz: + type: integer + numberOfCores: + type: integer + gpuItems: + type: array + items: + type: object + properties: + name: + type: string + clockRateMhz: + type: integer + numberOfCores: + type: integer + memoryItems: + type: array + items: + type: object + properties: + name: + type: string + speedMbPerS: + type: integer + sizeMb: + type: integer + storageItems: + type: array + items: + type: integer + properties: + name: + type: string + speedMbPerS: + type: integer + sizeMb: + type: integer + Portfolio: + type: object + properties: + _id: + type: string + projectId: + type: string + name: + type: string + scenarioIds: + type: array + items: + type: string + targets: type: object properties: - _id: - type: string - projectId: - type: string - name: - type: string - scenarioIds: - type: array - items: - type: string - targets: - type: object - properties: - enabledMetrics: - type: array - items: - type: string - repeatsPerScenario: - type: integer - Scenario: + enabledMetrics: + type: array + items: + type: string + repeatsPerScenario: + type: integer + Scenario: + type: object + properties: + _id: + type: string + portfolioId: + type: string + name: + type: string + simulation: type: object properties: - _id: - type: string - portfolioId: - type: string - name: - type: string - simulation: - type: object - properties: - state: - type: string - trace: - type: object - properties: - traceId: - type: string - loadSamplingFraction: - type: number - topology: - type: object - properties: - topologyId: - type: string - operational: - type: object - properties: - failuresEnabled: - type: boolean - performanceInterferenceEnabled: - type: boolean - schedulerName: - type: string - Trace: + state: + type: string + trace: type: object properties: - _id: - type: string - name: - type: string - path: - type: string - type: - type: string - User: + traceId: + type: string + loadSamplingFraction: + type: number + topology: type: object properties: - _id: - type: string - googleId: - type: integer - email: - type: string - givenName: - type: string - familyName: - type: string - authorizations: - type: array - items: - type: object - properties: - projectId: - type: string - authorizationLevel: - type: string + topologyId: + type: string + operational: + type: object + properties: + failuresEnabled: + type: boolean + performanceInterferenceEnabled: + type: boolean + schedulerName: + type: string + Trace: + type: object + properties: + _id: + type: string + name: + type: string + path: + type: string + type: + type: string + User: + type: object + properties: + _id: + type: string + googleId: + type: integer + email: + type: string + givenName: + type: string + familyName: + type: string + authorizations: + type: array + items: + type: object + properties: + projectId: + type: string + authorizationLevel: + type: string -- cgit v1.2.3 From 9416edf18b2ae9d737e970ad22a79d86935de1c0 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 17 Jul 2020 11:26:22 +0200 Subject: Update README for renamed API subproject --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66ffd22d..9ba77427 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ OpenDC is a project by the [@Large Research Group](http://atlarge-research.com). ## Architecture -OpenDC consists of four components: a Kotlin [simulator](/simulator), a MongoDB database, a Python Flask [web server](/api), and a React.js [frontend](/frontend), each in their own subdirectories. +OpenDC consists of four components: a Kotlin [simulator](/simulator), a MongoDB database, a Python Flask [API](/api), and a React.js [frontend](/frontend), each in their own subdirectories.

OpenDC Component Diagram -- cgit v1.2.3 From 9f85e80e40a663e3ebaf46a16f27332c4b7f0b53 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 17 Jul 2020 11:38:39 +0200 Subject: Enable support for failures and perf. interference --- .../kotlin/com/atlarge/opendc/runner/web/Main.kt | 33 ++++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt index d70ad6bd..fe1913f8 100644 --- a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt @@ -10,6 +10,7 @@ import com.atlarge.opendc.experiments.sc20.experiment.monitor.ParquetExperimentM import com.atlarge.opendc.experiments.sc20.experiment.processTrace import com.atlarge.opendc.experiments.sc20.trace.Sc20ParquetTraceReader import com.atlarge.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader +import com.atlarge.opendc.format.trace.sc20.Sc20PerformanceInterferenceReader import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.file @@ -136,18 +137,30 @@ class RunnerCli : CliktCommand(name = "runner") { */ private suspend fun runScenario(portfolio: Document, scenario: Document, topologies: MongoCollection) { val id = scenario.getString("_id") - val traceReader = Sc20RawParquetTraceReader( - File( - tracePath, - scenario.getEmbedded(listOf("trace", "traceId"), String::class.java) - ) + + logger.info { "Constructing performance interference model" } + + val traceDir = File( + tracePath, + scenario.getEmbedded(listOf("trace", "traceId"), String::class.java) ) + val traceReader = Sc20RawParquetTraceReader(traceDir) + val performanceInterferenceReader = let { + val path = File(traceDir, "performance-interference-model.json") + val enabled = scenario.getEmbedded(listOf("operational", "performanceInterferenceEnabled"), Boolean::class.java) + + if (!enabled || !path.exists()) { + return@let null + } + + path.inputStream().use { Sc20PerformanceInterferenceReader(it) } + } val targets = portfolio.get("targets", Document::class.java) repeat(targets.getInteger("repeatsPerScenario")) { logger.info { "Starting repeat $it" } - runRepeat(scenario, it, topologies, traceReader) + runRepeat(scenario, it, topologies, traceReader, performanceInterferenceReader) } logger.info { "Finished scenario $id" } @@ -160,7 +173,8 @@ class RunnerCli : CliktCommand(name = "runner") { scenario: Document, repeat: Int, topologies: MongoCollection, - traceReader: Sc20RawParquetTraceReader + traceReader: Sc20RawParquetTraceReader, + performanceInterferenceReader: Sc20PerformanceInterferenceReader? ) { val id = scenario.getString("_id") val seed = repeat @@ -189,9 +203,10 @@ class RunnerCli : CliktCommand(name = "runner") { else -> throw IllegalArgumentException("Unknown policy $policyName") } + val performanceInterferenceModel = performanceInterferenceReader?.construct(seeder) ?: emptyMap() val trace = Sc20ParquetTraceReader( listOf(traceReader), - emptyMap(), + performanceInterferenceModel, Workload(workloadName, workloadFraction), seed ) @@ -214,7 +229,7 @@ class RunnerCli : CliktCommand(name = "runner") { logger.debug("ENABLING failures") createFailureDomain( seeder.nextInt(), - operational.getDouble("failureFrequency"), + operational.get("failureFrequency", Number::class.java)?.toDouble() ?: 24.0 * 7, bareMetalProvisioner, chan ) -- cgit v1.2.3 From 0a895abfe307fbb6a28ceac6a07c5ac4863627fd Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 17 Jul 2020 17:25:45 +0200 Subject: Add data processing pipeline via Spark This change adds support for processing the experimental results by means of a Spark data processing pipeline. --- api/opendc/api/v2/schedulers/endpoint.py | 2 +- docker-compose.yml | 55 +++++- simulator/.dockerignore | 11 ++ simulator/Dockerfile | 2 +- .../opendc/opendc-runner-web/build.gradle.kts | 8 +- .../kotlin/com/atlarge/opendc/runner/web/Main.kt | 30 +++- .../atlarge/opendc/runner/web/ResultProcessor.kt | 187 +++++++++++++++++++++ .../atlarge/opendc/runner/web/ScenarioManager.kt | 44 +++-- .../src/main/resources/log4j2.xml | 3 + 9 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 simulator/.dockerignore create mode 100644 simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ResultProcessor.kt diff --git a/api/opendc/api/v2/schedulers/endpoint.py b/api/opendc/api/v2/schedulers/endpoint.py index a96fdd88..127b5f1a 100644 --- a/api/opendc/api/v2/schedulers/endpoint.py +++ b/api/opendc/api/v2/schedulers/endpoint.py @@ -1,6 +1,6 @@ from opendc.util.rest import Response -SCHEDULERS = ['DEFAULT'] +SCHEDULERS = ['core-mem'] def GET(_): diff --git a/docker-compose.yml b/docker-compose.yml index 5e45ea59..ecd2fceb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,20 +39,64 @@ services: restart: on-failure networks: - backend + - spark depends_on: - mongo + - spark-master + - spark-worker volumes: - type: bind source: ./traces target: /home/gradle/simulator/traces - type: volume - source: results-volume - target: /home/gradle/simulator/results + source: results-volume + target: /results + # Override to root as the simulator won't be able to access the volume otherwise + user: root environment: - OPENDC_DB - OPENDC_DB_USERNAME - OPENDC_DB_PASSWORD - OPENDC_DB_HOST=mongo + - OPENDC_OUTPUT=/results + - OPENDC_SPARK=spark://spark-master:7077 + + spark-master: + image: docker.io/bitnami/spark:3-debian-10 + networks: + - spark + environment: + - SPARK_MODE=master + - SPARK_RPC_AUTHENTICATION_ENABLED=no + - SPARK_RPC_ENCRYPTION_ENABLED=no + - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no + - SPARK_SSL_ENABLED=no + # Comment out for public deployment + ports: + - "8080:8080" + - "7077:7077" + + spark-worker: + image: docker.io/bitnami/spark:3-debian-10 + networks: + - spark + depends_on: + - spark-master + volumes: + - type: volume + source: results-volume + target: /results + environment: + - SPARK_MODE=worker + - SPARK_MASTER_URL=spark://spark-master:7077 + - SPARK_WORKER_MEMORY=2G + - SPARK_WORKER_CORES=2 + - SPARK_RPC_AUTHENTICATION_ENABLED=no + - SPARK_RPC_ENCRYPTION_ENABLED=no + - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no + - SPARK_SSL_ENABLED=no + ports: + - "7081:8081" mongo: build: @@ -69,10 +113,10 @@ services: - backend # Comment out for public deployment ports: - - 27017:27017 + - "27017:27017" # Uncomment for persistent deployment - #volumes: - # - mongo-volume:/data/db + volumes: + - mongo-volume:/data/db mongo-express: image: mongo-express @@ -94,3 +138,4 @@ volumes: networks: backend: {} + spark: {} diff --git a/simulator/.dockerignore b/simulator/.dockerignore new file mode 100644 index 00000000..bcbdf2b0 --- /dev/null +++ b/simulator/.dockerignore @@ -0,0 +1,11 @@ +# IntelliJ +/out/ +.idea/ +*/out +*.iml +.idea_modules/ + +### Gradle +.gradle +build/ + diff --git a/simulator/Dockerfile b/simulator/Dockerfile index c923cddf..7daa8a2e 100644 --- a/simulator/Dockerfile +++ b/simulator/Dockerfile @@ -15,7 +15,7 @@ USER root WORKDIR $APP_HOME # Build the application -RUN gradle --no-daemon assemble installDist +RUN gradle --no-daemon :opendc:opendc-runner-web:installDist # Fix permissions RUN chown -R gradle:gradle $APP_HOME diff --git a/simulator/opendc/opendc-runner-web/build.gradle.kts b/simulator/opendc/opendc-runner-web/build.gradle.kts index 52a59694..6f725de1 100644 --- a/simulator/opendc/opendc-runner-web/build.gradle.kts +++ b/simulator/opendc/opendc-runner-web/build.gradle.kts @@ -42,8 +42,14 @@ dependencies { implementation("com.github.ajalt:clikt:2.8.0") implementation("io.github.microutils:kotlin-logging:1.7.10") + implementation("org.mongodb:mongodb-driver-sync:4.0.5") + implementation("org.apache.spark:spark-sql_2.12:3.0.0") { + exclude(group = "org.slf4j", module = "slf4j-log4j12") + exclude(group = "log4j") + } - runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1") runtimeOnly(project(":odcsim:odcsim-engine-omega")) + runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1") + runtimeOnly("org.apache.logging.log4j:log4j-1.2-api:2.13.1") } diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt index fe1913f8..86696887 100644 --- a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt @@ -109,11 +109,22 @@ class RunnerCli : CliktCommand(name = "runner") { */ private val outputPath by option( "--output", - help = "path to the results directory" + help = "path to the results directory", + envvar = "OPENDC_OUTPUT" ) .file(canBeFile = false) .defaultLazy { File("results/") } + /** + * The Spark master to connect to. + */ + private val spark by option( + "--spark", + help = "Spark master to connect to", + envvar = "OPENDC_SPARK" + ) + .required() + /** * Connect to the user-specified database. */ @@ -147,7 +158,8 @@ class RunnerCli : CliktCommand(name = "runner") { val traceReader = Sc20RawParquetTraceReader(traceDir) val performanceInterferenceReader = let { val path = File(traceDir, "performance-interference-model.json") - val enabled = scenario.getEmbedded(listOf("operational", "performanceInterferenceEnabled"), Boolean::class.java) + val operational = scenario.get("operational", Document::class.java) + val enabled = operational.getBoolean("performanceInterferenceEnabled") if (!enabled || !path.exists()) { return@let null @@ -163,7 +175,7 @@ class RunnerCli : CliktCommand(name = "runner") { runRepeat(scenario, it, topologies, traceReader, performanceInterferenceReader) } - logger.info { "Finished scenario $id" } + logger.info { "Finished simulation for scenario $id" } } /** @@ -266,13 +278,15 @@ class RunnerCli : CliktCommand(name = "runner") { override fun run() = runBlocking(Dispatchers.Default) { logger.info { "Starting OpenDC web runner" } - logger.info { "Connecting to MongoDB instance" } val database = createDatabase() val manager = ScenarioManager(database.getCollection("scenarios")) val portfolios = database.getCollection("portfolios") val topologies = database.getCollection("topologies") + logger.info { "Loading Spark" } + val resultProcessor = ResultProcessor(spark, outputPath) + logger.info { "Watching for queued scenarios" } while (true) { @@ -302,7 +316,13 @@ class RunnerCli : CliktCommand(name = "runner") { try { val portfolio = portfolios.find(Filters.eq("_id", scenario.getString("portfolioId"))).first()!! runScenario(portfolio, scenario, topologies) - manager.finish(id) + + logger.info { "Starting result processing" } + + val result = resultProcessor.process(id) + manager.finish(id, result) + + logger.info { "Successfully finished scenario $id" } } catch (e: Exception) { logger.warn(e) { "Scenario failed to finish" } manager.fail(id) diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ResultProcessor.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ResultProcessor.kt new file mode 100644 index 00000000..39092653 --- /dev/null +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ResultProcessor.kt @@ -0,0 +1,187 @@ +package com.atlarge.opendc.runner.web + +import java.io.File +import org.apache.spark.sql.Column +import org.apache.spark.sql.Dataset +import org.apache.spark.sql.Row +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.functions.* + +/** + * A helper class for processing the experiment results using Apache Spark. + */ +class ResultProcessor(private val master: String, private val outputPath: File) { + /** + * Process the results of the scenario with the given [id]. + */ + fun process(id: String): Result { + val spark = SparkSession.builder() + .master(master) + .appName("opendc-simulator-$id") + .config("spark.driver.bindAddress", "0.0.0.0") // Needed to allow the worker to connect to driver + .orCreate + + try { + val hostMetrics = spark.read().parquet(File(outputPath, "host-metrics/scenario_id=$id").path) + val provisionerMetrics = spark.read().parquet(File(outputPath, "provisioner-metrics/scenario_id=$id").path) + val res = aggregate(hostMetrics, provisionerMetrics).first() + + return Result( + res.getList(1), + res.getList(2), + res.getList(3), + res.getList(4), + res.getList(5), + res.getList(6), + res.getList(7), + res.getList(8), + res.getList(9), + res.getList(10), + res.getList(11), + res.getList(12), + res.getList(13), + res.getList(14), + res.getList(15) + ) + } finally { + spark.close() + } + } + + data class Result( + val totalRequestedBurst: List, + val totalGrantedBurst: List, + val totalOvercommittedBurst: List, + val totalInterferedBurst: List, + val meanCpuUsage: List, + val meanCpuDemand: List, + val meanNumDeployedImages: List, + val maxNumDeployedImages: List, + val totalPowerDraw: List, + val totalFailureSlices: List, + val totalFailureVmSlices: List, + val totalVmsSubmitted: List, + val totalVmsQueued: List, + val totalVmsFinished: List, + val totalVmsFailed: List + ) + + /** + * Perform aggregation of the experiment results. + */ + private fun aggregate(hostMetrics: Dataset, provisionerMetrics: Dataset): Dataset { + // Extrapolate the duration of the entries to span the entire trace + val hostMetricsExtra = hostMetrics + .withColumn("slice_counts", floor(col("duration") / lit(sliceLength))) + .withColumn("power_draw", col("power_draw") * col("slice_counts")) + .withColumn("state_int", states[col("state")]) + .withColumn("state_opposite_int", oppositeStates[col("state")]) + .withColumn("cpu_usage", col("cpu_usage") * col("slice_counts") * col("state_opposite_int")) + .withColumn("cpu_demand", col("cpu_demand") * col("slice_counts")) + .withColumn("failure_slice_count", col("slice_counts") * col("state_int")) + .withColumn("failure_vm_slice_count", col("slice_counts") * col("state_int") * col("vm_count")) + + // Process all data in a single run + val hostMetricsGrouped = hostMetricsExtra.groupBy("run_id") + + // Aggregate the summed total metrics + val systemMetrics = hostMetricsGrouped.agg( + sum("requested_burst").alias("total_requested_burst"), + sum("granted_burst").alias("total_granted_burst"), + sum("overcommissioned_burst").alias("total_overcommitted_burst"), + sum("interfered_burst").alias("total_interfered_burst"), + sum("power_draw").alias("total_power_draw"), + sum("failure_slice_count").alias("total_failure_slices"), + sum("failure_vm_slice_count").alias("total_failure_vm_slices") + ) + + // Aggregate metrics per host + val hvMetrics = hostMetrics + .groupBy("run_id", "host_id") + .agg( + sum("cpu_usage").alias("mean_cpu_usage"), + sum("cpu_demand").alias("mean_cpu_demand"), + avg("vm_count").alias("mean_num_deployed_images"), + count(lit(1)).alias("num_rows") + ) + .withColumn("mean_cpu_usage", col("mean_cpu_usage") / col("num_rows")) + .withColumn("mean_cpu_demand", col("mean_cpu_demand") / col("num_rows")) + .groupBy("run_id") + .agg( + avg("mean_cpu_usage").alias("mean_cpu_usage"), + avg("mean_cpu_demand").alias("mean_cpu_demand"), + avg("mean_num_deployed_images").alias("mean_num_deployed_images"), + max("mean_num_deployed_images").alias("max_num_deployed_images") + ) + + // Group the provisioner metrics per run + val provisionerMetricsGrouped = provisionerMetrics.groupBy("run_id") + + // Aggregate the provisioner metrics + val provisionerMetricsAggregated = provisionerMetricsGrouped.agg( + max("vm_total_count").alias("total_vms_submitted"), + max("vm_waiting_count").alias("total_vms_queued"), + max("vm_active_count").alias("total_vms_running"), + max("vm_inactive_count").alias("total_vms_finished"), + max("vm_failed_count").alias("total_vms_failed") + ) + + // Join the results into a single data frame + return systemMetrics + .join(hvMetrics, "run_id") + .join(provisionerMetricsAggregated, "run_id") + .select( + col("total_requested_burst"), + col("total_granted_burst"), + col("total_overcommitted_burst"), + col("total_interfered_burst"), + col("mean_cpu_usage"), + col("mean_cpu_demand"), + col("mean_num_deployed_images"), + col("max_num_deployed_images"), + col("total_power_draw"), + col("total_failure_slices"), + col("total_failure_vm_slices"), + col("total_vms_submitted"), + col("total_vms_queued"), + col("total_vms_finished"), + col("total_vms_failed") + ) + .groupBy(lit(1)) + .agg( + // TODO Check if order of values is correct + collect_list(col("total_requested_burst")).alias("total_requested_burst"), + collect_list(col("total_granted_burst")).alias("total_granted_burst"), + collect_list(col("total_overcommitted_burst")).alias("total_overcommitted_burst"), + collect_list(col("total_interfered_burst")).alias("total_interfered_burst"), + collect_list(col("mean_cpu_usage")).alias("mean_cpu_usage"), + collect_list(col("mean_cpu_demand")).alias("mean_cpu_demand"), + collect_list(col("mean_num_deployed_images")).alias("mean_num_deployed_images"), + collect_list(col("max_num_deployed_images")).alias("max_num_deployed_images"), + collect_list(col("total_power_draw")).alias("total_power_draw"), + collect_list(col("total_failure_slices")).alias("total_failure_slices"), + collect_list(col("total_failure_vm_slices")).alias("total_failure_vm_slices"), + collect_list(col("total_vms_submitted")).alias("total_vms_submitted"), + collect_list(col("total_vms_queued")).alias("total_vms_queued"), + collect_list(col("total_vms_finished")).alias("total_vms_finished"), + collect_list(col("total_vms_failed")).alias("total_vms_failed") + ) + } + + // Spark helper functions + operator fun Column.times(other: Column): Column = `$times`(other) + operator fun Column.div(other: Column): Column = `$div`(other) + operator fun Column.get(other: Column): Column = this.apply(other) + + val sliceLength = 5 * 60 * 1000 + val states = map( + lit("ERROR"), lit(1), + lit("ACTIVE"), lit(0), + lit("SHUTOFF"), lit(0) + ) + val oppositeStates = map( + lit("ERROR"), lit(0), + lit("ACTIVE"), lit(1), + lit("SHUTOFF"), lit(1) + ) +} diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt index 0f375385..40ffd282 100644 --- a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/ScenarioManager.kt @@ -30,7 +30,7 @@ class ScenarioManager(private val collection: MongoCollection) { ), Updates.combine( Updates.set("simulation.state", "RUNNING"), - Updates.set("simulation.time", Instant.now()) + Updates.set("simulation.heartbeat", Instant.now()) ) ) return res != null @@ -45,7 +45,7 @@ class ScenarioManager(private val collection: MongoCollection) { Filters.eq("_id", id), Filters.eq("simulation.state", "RUNNING") ), - Updates.set("simulation.time", Instant.now()) + Updates.set("simulation.heartbeat", Instant.now()) ) } @@ -54,24 +54,40 @@ class ScenarioManager(private val collection: MongoCollection) { */ fun fail(id: String) { collection.findOneAndUpdate( - Filters.and( - Filters.eq("_id", id), - Filters.eq("simulation.state", "FAILED") - ), - Updates.set("simulation.time", Instant.now()) + Filters.eq("_id", id), + Updates.combine( + Updates.set("simulation.state", "FAILED"), + Updates.set("simulation.heartbeat", Instant.now()) + ) ) } /** - * Mark the scenario as finished. + * Persist the specified results. */ - fun finish(id: String) { + fun finish(id: String, result: ResultProcessor.Result) { collection.findOneAndUpdate( - Filters.and( - Filters.eq("_id", id), - Filters.eq("simulation.state", "FINISHED") - ), - Updates.set("simulation.time", Instant.now()) + Filters.eq("_id", id), + Updates.combine( + Updates.set("simulation.state", "FINISHED"), + Updates.unset("simulation.time"), + Updates.set("results.total_requested_burst", result.totalRequestedBurst), + Updates.set("results.total_granted_burst", result.totalGrantedBurst), + Updates.set("results.total_overcommitted_burst", result.totalOvercommittedBurst), + Updates.set("results.total_interfered_burst", result.totalInterferedBurst), + Updates.set("results.mean_cpu_usage", result.meanCpuUsage), + Updates.set("results.mean_cpu_demand", result.meanCpuDemand), + Updates.set("results.mean_num_deployed_images", result.meanNumDeployedImages), + Updates.set("results.max_num_deployed_images", result.maxNumDeployedImages), + Updates.set("results.max_num_deployed_images", result.maxNumDeployedImages), + Updates.set("results.total_power_draw", result.totalPowerDraw), + Updates.set("results.total_failure_slices", result.totalFailureSlices), + Updates.set("results.total_failure_vm_slices", result.totalFailureVmSlices), + Updates.set("results.total_vms_submitted", result.totalVmsSubmitted), + Updates.set("results.total_vms_queued", result.totalVmsQueued), + Updates.set("results.total_vms_finished", result.totalVmsFinished), + Updates.set("results.total_vms_failed", result.totalVmsFailed) + ) ) } } diff --git a/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml b/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml index b5a2bbb5..1d873554 100644 --- a/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml +++ b/simulator/opendc/opendc-runner-web/src/main/resources/log4j2.xml @@ -42,6 +42,9 @@ + + + -- cgit v1.2.3 From bde8b51fc40a02e6e8514ff428a748a133502c34 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 18 Jul 2020 16:47:32 +0200 Subject: Default to local Spark instance --- docker-compose.yml | 42 ---------------------- .../kotlin/com/atlarge/opendc/runner/web/Main.kt | 4 +-- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ecd2fceb..d4954f58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,11 +39,8 @@ services: restart: on-failure networks: - backend - - spark depends_on: - mongo - - spark-master - - spark-worker volumes: - type: bind source: ./traces @@ -59,44 +56,6 @@ services: - OPENDC_DB_PASSWORD - OPENDC_DB_HOST=mongo - OPENDC_OUTPUT=/results - - OPENDC_SPARK=spark://spark-master:7077 - - spark-master: - image: docker.io/bitnami/spark:3-debian-10 - networks: - - spark - environment: - - SPARK_MODE=master - - SPARK_RPC_AUTHENTICATION_ENABLED=no - - SPARK_RPC_ENCRYPTION_ENABLED=no - - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no - - SPARK_SSL_ENABLED=no - # Comment out for public deployment - ports: - - "8080:8080" - - "7077:7077" - - spark-worker: - image: docker.io/bitnami/spark:3-debian-10 - networks: - - spark - depends_on: - - spark-master - volumes: - - type: volume - source: results-volume - target: /results - environment: - - SPARK_MODE=worker - - SPARK_MASTER_URL=spark://spark-master:7077 - - SPARK_WORKER_MEMORY=2G - - SPARK_WORKER_CORES=2 - - SPARK_RPC_AUTHENTICATION_ENABLED=no - - SPARK_RPC_ENCRYPTION_ENABLED=no - - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no - - SPARK_SSL_ENABLED=no - ports: - - "7081:8081" mongo: build: @@ -138,4 +97,3 @@ volumes: networks: backend: {} - spark: {} diff --git a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt index 86696887..0ff9b870 100644 --- a/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt +++ b/simulator/opendc/opendc-runner-web/src/main/kotlin/com/atlarge/opendc/runner/web/Main.kt @@ -123,7 +123,7 @@ class RunnerCli : CliktCommand(name = "runner") { help = "Spark master to connect to", envvar = "OPENDC_SPARK" ) - .required() + .default("local[*]") /** * Connect to the user-specified database. @@ -284,7 +284,7 @@ class RunnerCli : CliktCommand(name = "runner") { val portfolios = database.getCollection("portfolios") val topologies = database.getCollection("topologies") - logger.info { "Loading Spark" } + logger.info { "Launching Spark" } val resultProcessor = ResultProcessor(spark, outputPath) logger.info { "Watching for queued scenarios" } -- cgit v1.2.3 From 55c7dd85f2cf215c302c4bb9f21a15d9dc2b489d Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 18 Jul 2020 17:22:47 +0200 Subject: Make simulator image leaner This change updates the Dockerfile for the simulator to reduce its size. By using Docker stages, we can split the build image from the runtime image that only contains the runtime binaries. --- simulator/Dockerfile | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/simulator/Dockerfile b/simulator/Dockerfile index 7daa8a2e..852809b3 100644 --- a/simulator/Dockerfile +++ b/simulator/Dockerfile @@ -1,27 +1,11 @@ FROM gradle:jdk14 MAINTAINER OpenDC Maintainers -# Set the home directory to our gradle user's home. -ENV HOME=/home/gradle -ENV APP_HOME=$HOME/simulator - -# Copy OpenDC simulator -COPY ./ $APP_HOME - -# Build as root -USER root - -# Set the working directory to the simulator -WORKDIR $APP_HOME - -# Build the application -RUN gradle --no-daemon :opendc:opendc-runner-web:installDist - -# Fix permissions -RUN chown -R gradle:gradle $APP_HOME - -# Downgrade user -USER gradle - -# Start the Gradle application on run -CMD opendc/opendc-runner-web/build/install/opendc-runner-web/bin/opendc-runner-web +COPY ./ /simulator +RUN cd /simulator/ \ + && gradle --no-daemon :opendc:opendc-runner-web:installDist + +FROM openjdk:14 +COPY --from=0 /simulator/opendc/opendc-runner-web/build/install /simulator +WORKDIR /simulator +CMD opendc-runner-web/bin/opendc-runner-web -- cgit v1.2.3 From 8ecb607dc6b54ff7a37fc0fea4f1a896dc5e7015 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sun, 19 Jul 2020 16:37:49 +0200 Subject: Cache build artifacts for Docker build --- simulator/.dockerignore | 9 ++++----- simulator/.gitignore | 2 +- simulator/Dockerfile | 34 +++++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/simulator/.dockerignore b/simulator/.dockerignore index bcbdf2b0..816d338c 100644 --- a/simulator/.dockerignore +++ b/simulator/.dockerignore @@ -1,11 +1,10 @@ -# IntelliJ -/out/ +.git + .idea/ -*/out +**/out *.iml .idea_modules/ -### Gradle .gradle -build/ +**/build/ diff --git a/simulator/.gitignore b/simulator/.gitignore index 4ec6f778..917f2e6a 100644 --- a/simulator/.gitignore +++ b/simulator/.gitignore @@ -38,7 +38,7 @@ data/ # IntelliJ /out/ .idea/ -*/out +**/out *.iml # mpeltonen/sbt-idea plugin diff --git a/simulator/Dockerfile b/simulator/Dockerfile index 852809b3..e42c7f14 100644 --- a/simulator/Dockerfile +++ b/simulator/Dockerfile @@ -1,11 +1,31 @@ -FROM gradle:jdk14 +FROM openjdk:14-slim AS staging MAINTAINER OpenDC Maintainers -COPY ./ /simulator -RUN cd /simulator/ \ - && gradle --no-daemon :opendc:opendc-runner-web:installDist +# Build staging artifacts for dependency caching +COPY ./ /app +WORKDIR /app +RUN mkdir /staging \ + && cp -r buildSrc/ /staging \ + && cp gradle.properties /staging 2>/dev/null | true \ + && find -name "*.gradle.kts" | xargs cp --parents -t /staging -FROM openjdk:14 -COPY --from=0 /simulator/opendc/opendc-runner-web/build/install /simulator -WORKDIR /simulator +FROM openjdk:14-slim AS builder + +# Obtain (cache) Gradle wrapper +COPY gradlew /app/ +COPY gradle /app/gradle +WORKDIR /app +RUN ./gradlew --version + +# Install (cache) project dependencies only +COPY --from=staging /staging/ /app/ +RUN ./gradlew clean build --no-daemon > /dev/null 2>&1 || true + +# Build project +COPY ./ /app/ +RUN ./gradlew --no-daemon :opendc:opendc-runner-web:installDist + +FROM openjdk:14-slim +COPY --from=builder /app/opendc/opendc-runner-web/build/install /app +WORKDIR /app CMD opendc-runner-web/bin/opendc-runner-web -- cgit v1.2.3 From 4f5c582927574928c7f9df96ae52b448ba77028b Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Mon, 20 Jul 2020 11:28:45 +0200 Subject: Add port forwarding for frontend dev setups --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index d4954f58..4b9856e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,9 @@ services: build: ./api image: api restart: on-failure + # Comment out these 2 lines for deployment + ports: + - "8081:8081" networks: - backend depends_on: -- cgit v1.2.3 From b27510cbe9de29edb0d1d8be58a49383b836ded5 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 20 Jul 2020 11:55:31 +0200 Subject: Add deployment information regarding traces --- README.md | 3 +++ docker-compose.yml | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ba77427..0ca14867 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ OPENDC_ROOT_DIR=/your/path/to/opendc OPENDC_SERVER_BASE_URL=http://localhost:8081 ``` +Afterwards, you should also create a `traces/` directory in which you place the VM and workflow traces you want to +experiment with. + If you plan to publicly deploy, please also tweak the other settings. In that case, also check the `docker-compose.yml` for further instructions. Now, start the server: diff --git a/docker-compose.yml b/docker-compose.yml index 4b9856e4..6b5c979c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,8 +51,6 @@ services: - type: volume source: results-volume target: /results - # Override to root as the simulator won't be able to access the volume otherwise - user: root environment: - OPENDC_DB - OPENDC_DB_USERNAME @@ -76,7 +74,6 @@ services: # Comment out for public deployment ports: - "27017:27017" - # Uncomment for persistent deployment volumes: - mongo-volume:/data/db @@ -95,7 +92,6 @@ services: volumes: mongo-volume: - external: false results-volume: networks: -- cgit v1.2.3 From 2d62b339a1ee532d85152e9e4ecfb11048af1923 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 20 Jul 2020 12:20:57 +0200 Subject: Fix docker-compose configuration --- docker-compose.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6b5c979c..c3e62317 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,12 @@ services: - REACT_APP_OAUTH_CLIENT_ID=${OPENDC_OAUTH_CLIENT_ID} image: frontend restart: on-failure - ports: - - "8081:80" networks: - backend + depends_on: + - api + ports: + - "8081:80" api: build: ./api @@ -18,7 +20,7 @@ services: restart: on-failure # Comment out these 2 lines for deployment ports: - - "8081:8081" + - "8082:8081" networks: - backend depends_on: @@ -47,7 +49,7 @@ services: volumes: - type: bind source: ./traces - target: /home/gradle/simulator/traces + target: /app/traces - type: volume source: results-volume target: /results @@ -85,7 +87,7 @@ services: depends_on: - mongo ports: - - 8082:8081 + - "8083:8081" environment: ME_CONFIG_MONGODB_ADMINUSERNAME: "${MONGO_INITDB_ROOT_USERNAME}" ME_CONFIG_MONGODB_ADMINPASSWORD: "${MONGO_INITDB_ROOT_PASSWORD}" -- cgit v1.2.3 From 2a5f50e591f5e9c1da5db2f2011c779a88121675 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Mon, 20 Jul 2020 12:41:37 +0200 Subject: Update proxy --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index f5ade772..174b2f39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "author": "Georgios Andreadis (https://gandreadis.com/)", "license": "MIT", "private": true, - "proxy": "http://localhost:8081", + "proxy": "http://localhost:8082", "dependencies": { "approximate-number": "~2.0.0", "classnames": "~2.2.5", -- cgit v1.2.3 From 8f707f36e70a5ef5758cd3fc1f7c4d18aa68a435 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Mon, 20 Jul 2020 14:52:36 +0200 Subject: Add small Bitbrains trace --- .gitignore | 3 +++ traces/bitbrains-small/meta.parquet | Bin 0 -> 2148 bytes traces/bitbrains-small/trace.parquet | Bin 0 -> 1672463 bytes 3 files changed, 3 insertions(+) create mode 100644 traces/bitbrains-small/meta.parquet create mode 100644 traces/bitbrains-small/trace.parquet diff --git a/.gitignore b/.gitignore index 3d504e99..45bd066c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ database/opendc_testing/* # Old credential setup file keys.json + +# Traces +traces/ diff --git a/traces/bitbrains-small/meta.parquet b/traces/bitbrains-small/meta.parquet new file mode 100644 index 00000000..ce7a812c Binary files /dev/null and b/traces/bitbrains-small/meta.parquet differ diff --git a/traces/bitbrains-small/trace.parquet b/traces/bitbrains-small/trace.parquet new file mode 100644 index 00000000..1d7ce882 Binary files /dev/null and b/traces/bitbrains-small/trace.parquet differ -- cgit v1.2.3 From dba8ee7e56896924f63c86f88400f3f6ced2d80a Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 20 Jul 2020 20:32:33 +0200 Subject: Fix reporting of experiment failures This change fixes an issue where exceptions thrown during a simulation run are swallowed by the experiment runner. --- .../experiments/sc20/reporter/ConsoleExperimentReporter.kt | 10 ++++++++++ .../experiments/sc20/runner/execution/ExperimentScheduler.kt | 3 +-- .../sc20/runner/execution/ThreadPoolExperimentScheduler.kt | 5 +---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt index f59402d5..b446abc8 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt @@ -30,6 +30,7 @@ import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionL import com.atlarge.opendc.experiments.sc20.runner.execution.ExperimentExecutionResult import me.tongfei.progressbar.ProgressBar import me.tongfei.progressbar.ProgressBarBuilder +import mu.KotlinLogging /** * A reporter that reports the experiment progress to the console. @@ -45,6 +46,11 @@ public class ConsoleExperimentReporter : ExperimentExecutionListener { */ private var total = 0 + /** + * The logger for this reporter. + */ + private val logger = KotlinLogging.logger {} + /** * The progress bar to keep track of the progress. */ @@ -69,6 +75,10 @@ public class ConsoleExperimentReporter : ExperimentExecutionListener { pb.close() } } + + if (result is ExperimentExecutionResult.Failed) { + logger.warn(result.throwable) { "Descriptor $descriptor failed" } + } } override fun executionStarted(descriptor: ExperimentDescriptor) {} diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt index 0346a7f8..96678abf 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt @@ -49,11 +49,10 @@ interface ExperimentScheduler : Closeable { * * @param descriptor The descriptor to execute. * @param context The context to execute the descriptor in. - * @return The results of the experiment trial. */ suspend operator fun invoke( descriptor: ExperimentDescriptor, context: ExperimentExecutionContext - ): ExperimentExecutionResult + ) } } diff --git a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt index a7c8ba4d..a8ee59a8 100644 --- a/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt +++ b/simulator/opendc/opendc-experiments-sc20/src/main/kotlin/com/atlarge/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt @@ -47,7 +47,7 @@ class ThreadPoolExperimentScheduler(parallelism: Int = Runtime.getRuntime().avai override suspend fun invoke( descriptor: ExperimentDescriptor, context: ExperimentExecutionContext - ): ExperimentExecutionResult = supervisorScope { + ) = supervisorScope { val listener = object : ExperimentExecutionListener { override fun descriptorRegistered(descriptor: ExperimentDescriptor) { @@ -70,10 +70,7 @@ class ThreadPoolExperimentScheduler(parallelism: Int = Runtime.getRuntime().avai try { withContext(dispatcher) { descriptor(newContext) - ExperimentExecutionResult.Success } - } catch (e: Throwable) { - ExperimentExecutionResult.Failed(e) } finally { tickets.release() } -- cgit v1.2.3 From 791b5d1e443f97adc756264878c3aae41ca0f748 Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Mon, 20 Jul 2020 15:19:14 +0200 Subject: Add Bitbrains starter trace to Mongo by default --- database/mongo-init-opendc-db.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/database/mongo-init-opendc-db.sh b/database/mongo-init-opendc-db.sh index 44fa75a3..3a4c4e9b 100644 --- a/database/mongo-init-opendc-db.sh +++ b/database/mongo-init-opendc-db.sh @@ -21,6 +21,20 @@ $MONGO_CMD --eval 'db.createCollection("scenarios");' $MONGO_CMD --eval 'db.createCollection("traces");' $MONGO_CMD --eval 'db.createCollection("prefabs");' +echo 'Loading default traces' + +$MONGO_CMD --eval 'db.traces.update( + {"_id": "bitbrains-small"}, + { + "$set": { + "_id": "bitbrains-small", + "name": "bitbrains-small", + "type": "VM", + } + }, + {"upsert": true} +);' + echo 'Loading test data' $MONGO_CMD --eval 'db.prefabs.insertOne( -- cgit v1.2.3