summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore6
-rw-r--r--.github/workflows/build.yml40
-rw-r--r--.github/workflows/publish.yml9
-rw-r--r--buildSrc/build.gradle.kts16
-rw-r--r--buildSrc/settings.gradle.kts10
-rw-r--r--buildSrc/src/main/kotlin/Libs.kt10
-rw-r--r--buildSrc/src/main/kotlin/jacoco-aggregation.gradle.kts34
-rw-r--r--codecov.yml9
-rw-r--r--database/Dockerfile6
-rw-r--r--database/mongo-init-opendc-db.sh36
-rw-r--r--database/mongo-init-prefabs-domain-specific.sh3334
-rwxr-xr-xdatabase/prefab.py112
-rwxr-xr-xdatabase/prefabs.py122
-rw-r--r--docker-compose.override.yml29
-rw-r--r--docker-compose.prod.yml2
-rw-r--r--docker-compose.yml52
-rw-r--r--docs/architecture.md20
-rw-r--r--gradle/libs.versions.toml55
l---------opendc-api-spec.yml1
-rw-r--r--opendc-web/opendc-web-api/.coveragerc5
-rw-r--r--opendc-web/opendc-web-api/.dockerignore8
-rw-r--r--opendc-web/opendc-web-api/.gitignore19
-rw-r--r--opendc-web/opendc-web-api/.pylintrc523
-rw-r--r--opendc-web/opendc-web-api/.style.yapf3
-rw-r--r--opendc-web/opendc-web-api/Dockerfile32
-rw-r--r--opendc-web/opendc-web-api/README.md122
-rwxr-xr-xopendc-web/opendc-web-api/app.py137
-rw-r--r--opendc-web/opendc-web-api/build.gradle.kts74
-rwxr-xr-xopendc-web/opendc-web-api/check.sh1
-rw-r--r--opendc-web/opendc-web-api/conftest.py45
-rw-r--r--opendc-web/opendc-web-api/docs/component-diagram.pngbin90161 -> 0 bytes
-rwxr-xr-xopendc-web/opendc-web-api/format.sh1
-rw-r--r--opendc-web/opendc-web-api/opendc/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/api/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/api/jobs.py105
-rw-r--r--opendc-web/opendc-web-api/opendc/api/portfolios.py153
-rw-r--r--opendc-web/opendc-web-api/opendc/api/prefabs.py123
-rw-r--r--opendc-web/opendc-web-api/opendc/api/projects.py224
-rw-r--r--opendc-web/opendc-web-api/opendc/api/scenarios.py86
-rw-r--r--opendc-web/opendc-web-api/opendc/api/schedulers.py46
-rw-r--r--opendc-web/opendc-web-api/opendc/api/topologies.py97
-rw-r--r--opendc-web/opendc-web-api/opendc/api/traces.py51
-rw-r--r--opendc-web/opendc-web-api/opendc/auth.py236
-rw-r--r--opendc-web/opendc-web-api/opendc/database.py97
-rw-r--r--opendc-web/opendc-web-api/opendc/exts.py91
-rw-r--r--opendc-web/opendc-web-api/opendc/models/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/models/model.py61
-rw-r--r--opendc-web/opendc-web-api/opendc/models/portfolio.py47
-rw-r--r--opendc-web/opendc-web-api/opendc/models/prefab.py31
-rw-r--r--opendc-web/opendc-web-api/opendc/models/project.py48
-rw-r--r--opendc-web/opendc-web-api/opendc/models/scenario.py93
-rw-r--r--opendc-web/opendc-web-api/opendc/models/topology.py108
-rw-r--r--opendc-web/opendc-web-api/opendc/models/trace.py16
-rw-r--r--opendc-web/opendc-web-api/opendc/util.py32
-rw-r--r--opendc-web/opendc-web-api/pytest.ini5
-rw-r--r--opendc-web/opendc-web-api/requirements.txt47
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/SimulationState.kt)10
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt95
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt89
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt134
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt58
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt38
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt107
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt92
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt58
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt39
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt93
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt76
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt157
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt90
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt86
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt53
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt48
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt51
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt45
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt43
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt65
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt77
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt59
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt82
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt60
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt88
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt81
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt104
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt86
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt69
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt128
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt127
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiResult.kt)27
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt120
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt40
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt38
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt74
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt26
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt83
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt107
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt38
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt48
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt149
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.pngbin0 -> 2825 bytes
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application-dev.properties28
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application-prod.properties29
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application-test.properties36
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application.properties48
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/init-dev.sql3
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt48
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt100
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt200
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt265
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt213
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt240
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt178
-rw-r--r--opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt304
-rw-r--r--opendc-web/opendc-web-api/static/schema.yml1631
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_jobs.py139
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_portfolios.py340
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_prefabs.py252
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_projects.py197
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_scenarios.py135
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_schedulers.py22
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_topologies.py140
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_traces.py40
-rw-r--r--opendc-web/opendc-web-client/build.gradle.kts (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Job.kt)29
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt73
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt58
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt52
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt63
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/AuthConfiguration.kt)18
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt66
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt42
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt40
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt140
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt54
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt62
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt40
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt43
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt47
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt59
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt145
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt50
-rw-r--r--opendc-web/opendc-web-proto/build.gradle.kts (renamed from opendc-web/opendc-web-ui/src/api/prefabs.js)24
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/JobState.kt53
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Machine.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt)7
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/MemoryUnit.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/MemoryUnit.kt)5
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/OperationalPhenomena.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/OperationalPhenomena.kt)7
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProcessingUnit.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ProcessingUnit.kt)5
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProtocolError.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTopology.kt)8
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Rack.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Rack.kt)11
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Room.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt)9
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/RoomTile.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt)9
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Targets.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/PortfolioTargets.kt)15
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Trace.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Portfolio.kt)20
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Workload.kt44
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt46
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Portfolio.kt43
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Scenario.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Scenario.kt)24
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Topology.kt (renamed from opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Topology.kt)22
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Job.kt40
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Portfolio.kt72
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Project.kt44
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/ProjectRole.kt43
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt86
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Topology.kt75
-rw-r--r--opendc-web/opendc-web-runner/Dockerfile (renamed from Dockerfile)0
-rw-r--r--opendc-web/opendc-web-runner/build.gradle.kts7
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiClient.kt179
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTrace.kt28
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/Main.kt57
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/ScenarioManager.kt66
-rw-r--r--opendc-web/opendc-web-runner/src/test/kotlin/org/opendc/web/client/ApiClientTest.kt264
-rw-r--r--opendc-web/opendc-web-ui/src/api/index.js2
-rw-r--r--opendc-web/opendc-web-ui/src/api/portfolios.js18
-rw-r--r--opendc-web/opendc-web-ui/src/api/projects.js6
-rw-r--r--opendc-web/opendc-web-ui/src/api/scenarios.js20
-rw-r--r--opendc-web/opendc-web-ui/src/api/topologies.js19
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppNavigation.js30
-rw-r--r--opendc-web/opendc-web-ui/src/components/context/ContextSelector.js21
-rw-r--r--opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss1
-rw-r--r--opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js24
-rw-r--r--opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/context/TopologySelector.js23
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js23
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js19
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js31
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProject.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewTopology.js16
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js27
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js11
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js19
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js9
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js15
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js1
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js7
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js4
-rw-r--r--opendc-web/opendc-web-ui/src/data/experiments.js8
-rw-r--r--opendc-web/opendc-web-ui/src/data/project.js114
-rw-r--r--opendc-web/opendc-web-ui/src/data/topology.js69
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js14
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js13
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js6
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js25
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js36
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js16
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/building.js8
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/index.js3
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/room.js6
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js4
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js4
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js4
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js4
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/topology.js9
-rw-r--r--opendc-web/opendc-web-ui/src/shapes.js146
-rw-r--r--opendc-web/opendc-web-ui/src/util/authorizations.js12
-rw-r--r--opendc-web/opendc-web-ui/src/util/topology-schema.js18
-rw-r--r--opendc-web/opendc-web-ui/src/util/unit-specifications.js26
-rw-r--r--settings.gradle.kts3
234 files changed, 7421 insertions, 10522 deletions
diff --git a/.dockerignore b/.dockerignore
index e4d96f71..bbc8efcc 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,7 +1,5 @@
-Dockerfile
-
.git
-
+.env
.idea/
**/out
*.iml
@@ -10,3 +8,5 @@ Dockerfile
.gradle
**/build
**/node_modules
+
+**/Dockerfile
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index becbbbed..bc30d584 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,45 +55,7 @@ jobs:
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }}
- files: ./build/reports/jacoco/report.xml
- flags: simulator
- build-api:
- name: Build API (Python ${{ matrix.python }})
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-latest]
- python: [3.9]
- defaults:
- run:
- working-directory: opendc-web/opendc-web-api
- steps:
- - uses: actions/checkout@v3
- - name: Set up Python
- uses: actions/setup-python@v3
- 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 --cov --cov-report=xml --junitxml=junit-report.xml
- - name: Publish report
- if: always()
- uses: mikepenz/action-junit-report@v3.0.1
- with:
- check_name: test (Python ${{ matrix.python }})
- report_paths: '**/junit-report.xml'
- github_token: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload code coverage
- uses: codecov/codecov-action@v2
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- files: opendc-web/opendc-web-api/coverage.xml
- flags: api
+ files: ./build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml
build-ui:
name: Build UI (Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 479b94f2..4e386603 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -9,8 +9,6 @@ jobs:
name: Push Simulator
runs-on: ubuntu-latest
steps:
- - name: Check out the repo
- uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
@@ -30,14 +28,13 @@ jobs:
uses: docker/build-push-action@v2
with:
push: true
+ file: opendc-web/opendc-web-runner/Dockerfile
repository: atlargeresearch/opendc
tags: ${{ steps.prep.outputs.tags }}
push-api:
name: Push API
runs-on: ubuntu-latest
steps:
- - name: Check out the repo
- uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
@@ -57,15 +54,13 @@ jobs:
uses: docker/build-push-action@v2
with:
push: true
- context: opendc-web/opendc-web-api
+ file: opendc-web/opendc-web-api/Dockerfile
repository: atlargeresearch/opendc-web-api
tags: ${{ steps.prep.outputs.tags }}
push-ui:
name: Push UI
runs-on: ubuntu-latest
steps:
- - name: Check out the repo
- uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 760f0cab..d62ade8c 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -31,10 +31,14 @@ repositories {
}
dependencies {
- implementation(kotlin("gradle-plugin", version = "1.6.10"))
- implementation("org.jlleitschuh.gradle:ktlint-gradle:10.2.1")
- implementation("org.jetbrains.kotlin:kotlin-allopen:1.6.10")
- implementation("me.champeau.jmh:jmh-gradle-plugin:0.6.6")
- implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.10")
- implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2")
+ implementation(libs.kotlin.gradle)
+ implementation(libs.kotlin.allopen)
+ implementation(libs.kotlin.noarg)
+ implementation(libs.ktlint.gradle)
+ implementation(libs.jmh.gradle)
+ implementation(libs.dokka.gradle)
+ implementation(libs.shadow)
+
+ implementation(libs.jandex.gradle)
+ implementation(libs.quarkus.gradle)
}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
index c9f9ab38..746d38f1 100644
--- a/buildSrc/settings.gradle.kts
+++ b/buildSrc/settings.gradle.kts
@@ -20,4 +20,12 @@
* SOFTWARE.
*/
-/* Intentionally left blank */
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt
index f538b1ad..6a73e1b9 100644
--- a/buildSrc/src/main/kotlin/Libs.kt
+++ b/buildSrc/src/main/kotlin/Libs.kt
@@ -26,14 +26,12 @@ import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
-import kotlin.properties.ReadOnlyProperty
/**
* This class makes the version catalog accessible for the build scripts until Gradle adds support for it.
*
* See https://github.com/gradle/gradle/issues/15383
*/
-@Suppress("UnstableApiUsage")
public class Libs(project: Project) {
/**
* The version catalog of the project.
@@ -41,16 +39,10 @@ public class Libs(project: Project) {
private val versionCatalog = project.extensions.getByType(VersionCatalogsExtension::class).named("libs")
/**
- * A delegate for obtaining configuration values from a [Project] instance.
- */
- private fun lib(name: String? = null): ReadOnlyProperty<Libs, String> =
- ReadOnlyProperty { _, property -> get(name ?: property.name) }
-
- /**
* Obtain the version for the specified [dependency][name].
*/
operator fun get(name: String): String {
- val dep = versionCatalog.findDependency(name).get().get()
+ val dep = versionCatalog.findLibrary(name).get().get()
return "${dep.module.group}:${dep.module.name}:${dep.versionConstraint.displayName}"
}
diff --git a/buildSrc/src/main/kotlin/jacoco-aggregation.gradle.kts b/buildSrc/src/main/kotlin/jacoco-aggregation.gradle.kts
index 5afd3e0d..7ae42cd2 100644
--- a/buildSrc/src/main/kotlin/jacoco-aggregation.gradle.kts
+++ b/buildSrc/src/main/kotlin/jacoco-aggregation.gradle.kts
@@ -22,7 +22,8 @@
plugins {
- jacoco
+ base
+ id("jacoco-report-aggregation")
}
repositories {
@@ -30,25 +31,20 @@ repositories {
mavenCentral()
}
-tasks.register<JacocoReport>("codeCoverageReport") {
- group = "Coverage reports"
- description = "Generates an aggregate report based on all subprojects"
-
- reports {
- xml.required.set(true)
- xml.outputLocation.set(file("${buildDir}/reports/jacoco/report.xml"))
-
- html.required.set(true)
- }
-
+dependencies {
subprojects {
- this@subprojects.plugins.withType<JacocoPlugin>().configureEach {
- this@subprojects.tasks.matching {
- it.extensions.findByType<JacocoTaskExtension>() != null }
- .configureEach {
- sourceSets(this@subprojects.the<SourceSetContainer>().named("main").get())
- executionData(this)
- }
+ plugins.withType<JacocoPlugin>().configureEach {
+ jacocoAggregation(this@subprojects)
}
+
}
}
+
+@Suppress("UnstableApiUsage")
+val codeCoverageReport by reporting.reports.creating(JacocoCoverageReport::class) {
+ testType.set(TestSuiteType.UNIT_TEST)
+}
+
+tasks.check {
+ dependsOn(codeCoverageReport.reportTask)
+}
diff --git a/codecov.yml b/codecov.yml
index 9f591a85..8b137891 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,8 +1 @@
-flags:
- simulator:
- ignore:
- - opendc-web/opendc-web-ui
- - opendc-web/opendc-web-api
- api:
- paths:
- - opendc-web/opendc-web-api
+
diff --git a/database/Dockerfile b/database/Dockerfile
deleted file mode 100644
index e4978da6..00000000
--- a/database/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM mongo:5.0
-MAINTAINER OpenDC Maintainers <opendc@atlarge-research.com>
-
-# Import init scripts
-ADD mongo-init-opendc-db.sh /docker-entrypoint-initdb.d
-ADD mongo-init-prefabs-domain-specific.sh /docker-entrypoint-initdb.d
diff --git a/database/mongo-init-opendc-db.sh b/database/mongo-init-opendc-db.sh
deleted file mode 100644
index d55b8990..00000000
--- a/database/mongo-init-opendc-db.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-echo 'Creating OpenDC user and database'
-
-mongo opendc --host localhost \
- --port 27017 \
- -u "$MONGO_INITDB_ROOT_USERNAME" \
- -p "$MONGO_INITDB_ROOT_PASSWORD" \
- --authenticationDatabase admin \
- --eval "db.createUser({user: '$OPENDC_DB_USERNAME', pwd: '$OPENDC_DB_PASSWORD', roles:[{role:'dbOwner', db: '$OPENDC_DB'}]});"
-
-MONGO_CMD="mongo $OPENDC_DB -u $OPENDC_DB_USERNAME -p $OPENDC_DB_PASSWORD --authenticationDatabase $OPENDC_DB"
-
-echo 'Creating collections'
-
-$MONGO_CMD --eval 'db.createCollection("authorizations");'
-$MONGO_CMD --eval 'db.createCollection("projects");'
-$MONGO_CMD --eval 'db.createCollection("topologies");'
-$MONGO_CMD --eval 'db.createCollection("portfolios");'
-$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}
-);'
diff --git a/database/mongo-init-prefabs-domain-specific.sh b/database/mongo-init-prefabs-domain-specific.sh
deleted file mode 100644
index 0a9326a7..00000000
--- a/database/mongo-init-prefabs-domain-specific.sh
+++ /dev/null
@@ -1,3334 +0,0 @@
-#!/bin/bash
-
-echo "Adding domain-specific prefabs"
-
-MONGO_CMD="mongo $OPENDC_DB -u $OPENDC_DB_USERNAME -p $OPENDC_DB_PASSWORD --authenticationDatabase $OPENDC_DB"
-
-echo "Adding HPC prefabs"
-
-# Dell R440
-$MONGO_CMD --eval 'db.prefabs.insertOne(
- {
- "_id" : 440,
- "name" : "Dell R440",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Dell R440",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Gold 6252",
- "clockRateMhz": 2100,
- "numberOfCores": 24,
- "energyConsumptionW": 150
- },
- {
- "name": "Intel Xeon Gold 6252",
- "clockRateMhz": 2100,
- "numberOfCores": 24,
- "energyConsumptionW": 150
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "SK Hynix RDIMM HMA84GR7MFR4N-VK",
- "speedMbPerS": 42656,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# Dell R640
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 640,
- "name" : "Dell R640",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Dell R640",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# Dell R740xd
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 740,
- "name" : "Dell R740xd",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Dell R740xd",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# Dell R940
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 940,
- "name" : "Dell R940",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Dell R940",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "Dell 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# Inspur NF5280M5
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 5280,
- "name" : "Inspur NF5280M5",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Inspur NF5280M5",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386ABG40M51-CAE",
- "speedMbPerS": 51200,
- "sizeMb": 262144,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },{
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },{
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },{
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- },
- {
- "name": "Intel 7.6TB D3-S4610 SSD SATA",
- "speedMbPerS": 550,
- "sizeMb": 7600000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# Inspur power systems FP5180G2
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 5180,
- "name" : "Inspur Power Systems FP5180G2",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "Inspur Power Systems FP5180G2",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "IBM POWER9 CP9M08",
- "clockRateMhz": 2750,
- "numberOfCores": 22,
- "energyConsumptionW": 190
- },
- {
- "name": "IBM POWER9 CP9M08",
- "clockRateMhz": 2750,
- "numberOfCores": 22,
- "energyConsumptionW": 190
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Micron LRDIMM MTA144ASQ16G72LSZ-2S6",
- "speedMbPerS": 42656,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- },
- {
- "name": "Intel 6.4TB D7-P5600 SSD SATA",
- "speedMbPerS": 7000,
- "sizeMb": 6400000,
- "energyConsumptionW": 20
- }
- ],
- }
- ]
- }
-});'
-
-# HPE Superdome Flex node
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 7873736633539,
- "name" : "HPE Superdome Flex Node",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "HPE Superdome Flex Node",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [
- {
- "name": "NVIDIA TESLA V100 32GB",
- "clockRateMhz": 1230,
- "numberOfCores": 5120,
- "energyConsumptionW": 250
- },
- {
- "name": "NVIDIA TESLA V100 32GB",
- "clockRateMhz": 1230,
- "numberOfCores": 5120,
- "energyConsumptionW": 250
- },
- {
- "name": "NVIDIA TESLA V100 32GB",
- "clockRateMhz": 1230,
- "numberOfCores": 5120,
- "energyConsumptionW": 250
- },
- {
- "name": "NVIDIA TESLA V100 32GB",
- "clockRateMhz": 1230,
- "numberOfCores": 5120,
- "energyConsumptionW": 250
- }
- ],
- "memories": [
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# HPE DL360 G10
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 36010,
- "name" : "HPE DL360 Gen10",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "HPE DL360 Gen10",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Gold 6248",
- "clockRateMhz": 2500,
- "numberOfCores": 20,
- "energyConsumptionW": 150
- },
- {
- "name": "Intel Xeon Gold 6248",
- "clockRateMhz": 2500,
- "numberOfCores": 20,
- "energyConsumptionW": 150
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung RDIMM M393A4K40CB2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 32768,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# HPE DL380 G10
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 38010,
- "name" : "HPE DL380 Gen10",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "HPE DL380 Gen10",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 1.92TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 1920000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
-
-# HPE DL580 G10
-$MONGO_CMD --eval 'db.prefabs.insertOne({
- "_id" : 58010,
- "name" : "HPE DL580 Gen10",
- "tags" : ["hpc"],
- "visibility" : "public",
- "rack" : {
- "name": "HPE DL580 Gen10",
- "capacity": "42",
- "powerCapacityW": "25000",
- "machines" : [
- {
- "position": 1,
- "cpus": [
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- },
- {
- "name": "Intel Xeon Platinum 8280M",
- "clockRateMhz": 2700,
- "numberOfCores": 28,
- "energyConsumptionW": 205
- }
- ],
- "gpus": [],
- "memories": [
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- },
- {
- "name": "Samsung LRDIMM M386AAG40MM2-CVF",
- "speedMbPerS": 46928,
- "sizeMb": 131072,
- "energyConsumptionW": 10
- }
- ],
- "storages": [
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- },
- {
- "name": "HPE 3.84TB SSD SATA",
- "speedMbPerS": 600,
- "sizeMb": 3840000,
- "energyConsumptionW": 10
- }
- ],
- }
- ]
- }
-});'
diff --git a/database/prefab.py b/database/prefab.py
deleted file mode 100755
index b993c7f0..00000000
--- a/database/prefab.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-#Change shebang to /usr/bin/python3 before using with docker
-# encoding: utf-8
-"""
-prefab
-
-CLI frontend for viewing, modifying and creating prefabs in OpenDC.
-
-"""
-import sys
-import prefabs
-
-def usage():
- print("Usage: prefab add <prefab>: imports a prefab from JSON")
- print(" list: lists all (public) prefabs")
- print(" export <prefab> [json|yaml]: exports the specified prefab to the specified filetype (with JSON used by default)")
- print(" clone <prefab> [new prefab name]: clones the specified prefab, giving the new prefab a name if specified")
- print(" remove <prefab>: removes the specified prefab from the database")
-
-def interactive(): #interactive CLI mode: recommended
- print("OpenDC Prefab CLI")
- running = True
- while(exit):
- print(">", end=" ")
- try:
- command = input()
- command = command.split()
- except EOFError as e:
- print("exit")
- print("bye!")
- exit()
- except KeyboardInterrupt as KI:
- print("\nbye!")
- exit()
- if(len(command) >= 1):
- if(command[0] == "exit"):
- print("bye!")
- exit()
- elif(command[0] == "list"): # decrypt
- prefabs.list()
- elif(command[0] == "help"): # decrypt
- usage()
- elif(command[0] == "add"):
- if(len(command) == 3):
- prefabs.add(command[1], command[2])
- else:
- prefabs.add(command[1], None)
- elif(command[0] == "clone"):
- if(len(command) == 3):
- prefabs.clone(command[1], command[2])
- else:
- prefabs.clone(command[1], None)
- elif(command[0] == "export"):
- #print(sys.argv[2])
- prefabs.export(command[1], "json")
- elif(command[0] == "remove"):
- print("WARNING: Doing so will permanently remove the specified prefab. \nThis action CANNOT be undone. Please type the name of the prefab to confirm deletion.")
- confirm = input()
- if confirm == command[1]:
- prefabs.remove(command[1])
- print(f'Prefab {command[1]} has been removed.')
- else:
- print("Confirmation failed. The prefab has not been removed.")
- else:
- print("prefabs: try 'help' for more information\n")
- else:
- print("prefabs: try 'help' for more information\n")
-
-
-def main():
- if(len(sys.argv) >= 2):
- if(sys.argv[1] == "list"): # decrypt
- prefabs.list()
- exit()
- #elif(sys.argv[1] == "-e"): # encrypt
- # encrypt(sys.argv[2], sys.argv[3], sys.argv[4])
- #elif(sys.argv[1] == "-v"): # verify
- # verify(sys.argv[2], sys.argv[3], sys.argv[4])
- elif(sys.argv[1] == "help"): # decrypt
- usage()
- exit()
- elif(sys.argv[1] == "add"):
- if(sys.argv[3]):
- prefabs.add(sys.argv[2], sys.argv[3])
- else:
- prefabs.add(sys.argv[2])
- exit()
- elif(sys.argv[1] == "export"):
- #print(sys.argv[2])
- prefabs.export(sys.argv[2], "json")
- exit()
- elif(sys.argv[1] == "remove"):
- print("WARNING: Doing so will permanently remove the specified prefab. \nThis action CANNOT be undone. Please type the name of the prefab to confirm deletion.")
- confirm = input()
- if confirm == sys.argv[2]:
- prefabs.remove(sys.argv[2])
- print(f'Prefab {sys.argv[2]} has been removed.')
- else:
- print("Confirmation failed. The prefab has not been removed.")
- exit()
- else:
- print("prefabs: try 'prefabs help' for more information\n")
- elif(len(sys.argv) == 1):
- interactive()
-
- else:
- # print "Incorrect number of arguments!\n"
- print("prefabs: try 'prefabs help' for more information\n")
-
-
-if __name__ == "__main__":
- main()
diff --git a/database/prefabs.py b/database/prefabs.py
deleted file mode 100755
index ed308d84..00000000
--- a/database/prefabs.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# encoding: utf-8
-"""
-prefabs
-
-Python Library for interacting with mongoDB prefabs collection.
-
-"""
-import urllib.parse
-import pprint
-import sys
-import os
-import json
-import re
-import ujson
-#import pyyaml
-
-from pymongo import MongoClient
-from bson.json_util import loads, dumps, RELAXED_JSON_OPTIONS, CANONICAL_JSON_OPTIONS
-
-#mongodb_opendc_db = os.environ['OPENDC_DB']
-#mongodb_opendc_user = os.environ['OPENDC_DB_USERNAME']
-#mongodb_opendc_password = os.environ['OPENDC_DB_PASSWORD']
-
-#if mongodb_opendc_db == None or mongodb_opendc_user == None or mongodb_opendc_password == None:
-# print("One or more environment variables are not set correctly. \nYou may experience issues connecting to the mongodb database.")
-
-user = urllib.parse.quote_plus('opendc') #TODO: replace this with environment variable
-password = urllib.parse.quote_plus('opendcpassword') #TODO: same as above
-database = urllib.parse.quote_plus('opendc')
-
-client = MongoClient('mongodb://%s:%s@localhost/default_db?authSource=%s' % (user, password, database))
-opendcdb = client.opendc
-prefabs_collection = opendcdb.prefabs
-
-
-def add(prefab_file, name):
- if(re.match(r"\w+(\\\ \w*)*\.json", prefab_file)):
- try:
- with open(prefab_file, "r") as json_file:
- json_prefab = json.load(json_file)
- #print(json_prefab)
- if name != None:
- json_prefab["name"] = name
- try:
- prefab_id = prefabs_collection.insert(json_prefab)
- except ConnectionFailure:
- print("ERROR: Could not connect to the mongoDB database.")
- except DuplicateKeyError:
- print("ERROR: A prefab with the same unique ID already exists in the database. \nPlease remove the '_id' before trying again.\nYour prefab has not been imported.")
- except:
- print("ERROR: A general error has occurred. Your prefab has not been imported.")
- if prefab_id != None:
- if name != None:
- print(f'Prefab "{name}" has been imported successfully.')
- else:
- print(f'Prefab "{prefab_file}" has been imported successfully.')
- except FileNotFoundError:
- print(f"ERROR: {prefab_file} could not be found in the specified path. No prefabs have been imported.")
- elif(re.match(r"\w+(\\\ \w*)*\.yml", prefab_file)):
- print("expecting a yaml file here")
- #yaml
- else:
- print("The filetype provided is an unsupported filetype.")
- #unsupported filetype
-
-def clone(prefab_name, new_name):
- bson = prefabs_collection.find_one({'name': prefab_name})
- json_string = dumps(bson) #convert BSON representation to JSON
- chosen_prefab = json.loads(json_string) #load as a JSON object
-
- chosen_prefab.pop("_id") # clean out our _id field from the export: mongo will generate a new one if this is imported back in
-
- if new_name != None:
- chosen_prefab["name"] = new_name
- try:
- prefab_id = prefabs_collection.insert_one(chosen_prefab)
- except ConnectionFailure:
- print("ERROR: Could not connect to the mongoDB database.")
- except:
- print("ERROR: A general error has occurred. Your selected prefab has not been cloned.")
- if prefab_id != None:
- if new_name != None:
- print(f'Prefab "{prefab_name}" has been cloned successfully as {new_name}.')
- else:
- print(f'Prefab "{prefab_name}" has been cloned successfully.')
-
-def export(prefab_name, type):
- bson = prefabs_collection.find_one({'name': prefab_name})
- json_string = dumps(bson) #convert BSON representation to JSON
- chosen_prefab = json.loads(json_string) #load as a JSON object
-
- chosen_prefab.pop("_id") # clean out our _id field from the export: mongo will generate a new one if this is imported back in
-
- with open(f'{prefab_name}.json', 'w', encoding='utf8') as f:
- json.dump(chosen_prefab, f, ensure_ascii=False, indent=4)
- print(f'Prefab {prefab_name} written to {os.getcwd()}/{prefab_name}.json.')
- #pprint.pprint(json_string)
- #pprint.pprint(json.loads(str(json_string)))
-
-def list():
- #TODO: why does it output in single quotations?
- cursor = prefabs_collection.find()
- prefabs = []
- for record in cursor:
- #pprint.pprint(record)
- #print(record)
- json_string = dumps(record, json_options=RELAXED_JSON_OPTIONS) ##pymongo retrieves BSON objects, which need to be converted to json for pythons json module
- prefabs.append(json.loads(json_string))
-
- #print(f'There are {str(len(prefabs))} prefabs in the database. They are:')
- print("Name Author")
- for prefab in prefabs:
- if(prefab['visibility'] == "private"):
- continue
- print(f"{prefab['name']} {prefab['author']}")
- #pprint.pprint(prefab)
-
-
-def remove(prefab_name):
- prefabs_collection.delete_one({'name': prefab_name})
-
-
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 5d104557..314adcb1 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -10,29 +10,38 @@ services:
NEXT_PUBLIC_API_BASE_URL: http://localhost:8081
api:
- build: opendc-web/opendc-web-api
+ build:
+ context: .
+ dockerfile: opendc-web/opendc-web-api/Dockerfile
ports:
- "8081:80"
environment:
SENTRY_ENVIRONMENT: "development"
- simulator:
- build: .
+ runner:
+ build:
+ context: .
+ dockerfile: opendc-web/opendc-web-runner/Dockerfile
environment:
SENTRY_ENVIRONMENT: "development"
- mongo:
+ postgres:
ports:
- - "27017:27017"
+ - "5432:5432"
- mongo-express:
- image: mongo-express
+ pgadmin:
+ image: dpage/pgadmin4
restart: on-failure
networks:
- backend
depends_on:
- - mongo
+ - postgres
ports:
- - "8082:8081"
+ - "5050:80"
environment:
- ME_CONFIG_MONGODB_URL: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017/"
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
+ PGADMIN_CONFIG_SERVER_MODE: 'False'
+
+volumes:
+ pgadmin:
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index c4f43298..1206ff9c 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -14,6 +14,6 @@ services:
environment:
SENTRY_ENVIRONMENT: "production"
- simulator:
+ runner:
environment:
SENTRY_ENVIRONMENT: "production"
diff --git a/docker-compose.yml b/docker-compose.yml
index 52209a5b..faaecc03 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,23 +19,16 @@ services:
networks:
- backend
depends_on:
- - mongo
+ - postgres
environment:
- - MONGO_INITDB_ROOT_USERNAME
- - MONGO_INITDB_ROOT_PASSWORD
- - MONGO_INITDB_DATABASE
- - OPENDC_DB
- - OPENDC_DB_USERNAME
- - OPENDC_DB_PASSWORD
- - OPENDC_DB_HOST=mongo
- - OPENDC_FLASK_SECRET
- - AUTH0_DOMAIN=${OPENDC_AUTH0_DOMAIN}
- - AUTH0_AUDIENCE=${OPENDC_AUTH0_AUDIENCE}
- - AUTH0_DOCS_CLIENT_ID=${OPENDC_AUTH0_DOCS_CLIENT_ID}
- - SENTRY_DSN=${OPENDC_API_SENTRY_DSN-}
- - SENTRY_ENVIRONMENT
+ OPENDC_DB_USERNAME: ${OPENDC_DB_USERNAME:?No database username specified}
+ OPENDC_DB_PASSWORD: ${OPENDC_DB_PASSWORD:?No database password specified}
+ OPENDC_DB_URL: jdbc:postgresql://postgres:5432/opendc
+ OPENDC_AUTH0_DOMAIN: ${OPENDC_AUTH0_DOMAIN:?No Auth0 domain specified}
+ OPENDC_AUTH0_AUDIENCE: ${OPENDC_AUTH0_AUDIENCE:?No Auth0 audience specified}
+ SENTRY_DSN: ${OPENDC_API_SENTRY_DSN-}
- simulator:
+ runner:
image: atlargeresearch/opendc:v2.1
restart: on-failure
networks:
@@ -47,31 +40,26 @@ services:
source: ./traces
target: /opt/opendc/traces
environment:
- - OPENDC_API_URL=${OPENDC_API_BASE_URL}
- - AUTH0_DOMAIN=${OPENDC_AUTH0_DOMAIN}
- - AUTH0_AUDIENCE=${OPENDC_AUTH0_AUDIENCE}
- - AUTH0_CLIENT_ID=${OPENDC_AUTH0_CLIENT_ID_RUNNER}
- - AUTH0_CLIENT_SECRET=${OPENDC_AUTH0_CLIENT_SECRET_RUNNER}
- - SENTRY_DSN=${OPENDC_SIMULATOR_SENTRY_DSN-}
- - SENTRY_ENVIRONMENT
+ OPENDC_API_URL: ${OPENDC_API_BASE_URL:-http://web:8080}
+ AUTH0_DOMAIN: ${OPENDC_AUTH0_DOMAIN:?No Auth0 domain specified}
+ AUTH0_AUDIENCE: ${OPENDC_AUTH0_AUDIENCE:?No Auth0 audience specified}
+ AUTH0_CLIENT_ID: ${OPENDC_AUTH0_CLIENT_ID_RUNNER:?No client id for runner}
+ AUTH0_CLIENT_SECRET: ${OPENDC_AUTH0_CLIENT_SECRET_RUNNER:?No client secret for runner}
+ SENTRY_DSN: ${OPENDC_SIMULATOR_SENTRY_DSN-}
- mongo:
- build: database
+ postgres:
+ image: postgres
restart: on-failure
environment:
- - MONGO_INITDB_ROOT_USERNAME
- - MONGO_INITDB_ROOT_PASSWORD
- - MONGO_INITDB_DATABASE
- - OPENDC_DB
- - OPENDC_DB_USERNAME
- - OPENDC_DB_PASSWORD
+ POSTGRES_USER: ${OPENDC_DB_USERNAME}
+ POSTGRES_PASSWORD: ${OPENDC_DB_PASSWORD}
networks:
- backend
volumes:
- - mongo-volume:/data/db
+ - postgres:/var/lib/postgresql/data
volumes:
- mongo-volume:
+ postgres:
networks:
backend: {}
diff --git a/docs/architecture.md b/docs/architecture.md
index 7e644ec1..c4609ae9 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,23 +1,21 @@
# Architecture
-OpenDC consists of four components: a Kotlin simulator, a MongoDB database, a Python
-Flask [API](/opendc-web/opendc-web-api), and a React.js [frontend](/opendc-web/opendc-web-ui).
+OpenDC consists of four components: a Kotlin simulator, a SQL database, a Quarkus-based
+[API](/opendc-web/opendc-web-api), and a React.js [frontend](/opendc-web/opendc-web-ui).
![OpenDC Component Diagram](./images/component-diagram.png)
On the frontend, users can construct a topology by specifying a datacenter's rooms, racks and machines, and create
-scenarios to see how a workload trace runs on that topology. The frontend communicates with the web server over
-SocketIO, through a custom REST request/response layer. For example, the frontend might make a `GET` request
-to `/api/v1/users/{userId}`, but this request is completed via SocketIO, not plain HTTP requests. Note that the API
-itself can also be accessed by HTTP.
+scenarios to see how a workload trace runs on that topology. The frontend communicates with the web server via a REST
+API over HTTP.
The (Swagger/OpenAPI compliant) API spec specifies what requests the frontend can make to the web server. To view this
specification, go to the [Swagger Editor](https://editor.swagger.io/) and paste in
-our [opendc-api-spec.yml](../opendc-api-spec.yml).
+our [API spec](https://api.opendc.org/q/openapi).
The web server receives API requests and processes them in the database. When the frontend requests to run a new
-scenario, the web server adds it to the `scenarios` collection in the database and sets its `state` as `QUEUED`.
+scenario, the web server adds it to the `scenarios` collection in the database and sets its `state` as `PENDING`.
-The simulator monitors the database for `QUEUED` scenarios, and simulates them as they are submitted. The results of the
-simulations are processed and aggregated in memory. Afterwards, the aggregated summary is written to the database, which
-the frontend can then again retrieve via the web server.
+The simulator monitors the database for `PENDING` scenarios, and simulates them as they are submitted. The results of
+the simulations are processed and aggregated in memory. Afterwards, the aggregated summary is written to the database,
+which the frontend can then again retrieve via the web server.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5fe5cc33..fbed7e7b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,25 +3,37 @@ classgraph = "4.8.138"
clikt = "3.3.0"
config = "1.4.1"
commons-math3 = "3.6.1"
+dokka = "1.6.10"
hadoop = "3.3.1"
jackson = "2.13.1"
+jandex-gradle = "0.11.0"
+jmh-gradle = "0.6.6"
+jakarta-validation = "2.0.2"
junit-jupiter = "5.8.2"
junit-platform = "1.8.2"
+kotlin = "1.6.10"
kotlin-logging = "2.1.21"
kotlinx-coroutines = "1.6.0"
-ktor = "1.6.7"
+ktlint-gradle = "10.2.1"
log4j = "2.17.1"
+microprofile-openapi = "3.0"
mockk = "1.12.2"
opentelemetry-main = "1.11.0"
-opentelemetry-metrics = "1.10.1-alpha"
-opentelemetry-semconv = "1.10.1-alpha"
+opentelemetry-metrics = "1.11.0-alpha"
+opentelemetry-semconv = "1.11.0-alpha"
parquet = "1.12.2"
progressbar = "0.9.2"
+quarkus = "2.7.5.Final"
+quarkus-junit5-mockk = "0.3.0"
sentry = "5.5.2"
+shadow = "7.1.2"
slf4j = "1.7.32"
[libraries]
# Kotlin
+kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlin-allopen = { module = "org.jetbrains.kotlin:kotlin-allopen", version.ref = "kotlin" }
+kotlin-noarg = { module = "org.jetbrains.kotlin:kotlin-noarg", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
# Logging
@@ -50,6 +62,7 @@ progressbar = { module = "me.tongfei:progressbar", version.ref = "progressbar" }
# Format
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" }
+jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
@@ -57,14 +70,40 @@ jackson-dataformat-csv = { module = "com.fasterxml.jackson.dataformat:jackson-da
parquet = { module = "org.apache.parquet:parquet-avro", version.ref = "parquet" }
config = { module = "com.typesafe:config", version.ref = "config" }
-# HTTP client
-ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
-ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
-ktor-client-jackson = { module = "io.ktor:ktor-client-jackson", version.ref = "ktor" }
-ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
+# Quarkus
+quarkus-gradle = { module = "io.quarkus:gradle-application-plugin", version.ref = "quarkus" }
+quarkus-bom = { module = "io.quarkus:quarkus-bom", version.ref = "quarkus" }
+quarkus-kotlin = { module = "io.quarkus:quarkus-kotlin" }
+quarkus-resteasy-core = { module = "io.quarkus:quarkus-resteasy" }
+quarkus-resteasy-jackson = { module = "io.quarkus:quarkus-resteasy-jackson" }
+quarkus-smallrye-openapi = { module = "io.quarkus:quarkus-smallrye-openapi" }
+quarkus-security = { module = "io.quarkus:quarkus-security" }
+quarkus-oidc = { module = "io.quarkus:quarkus-oidc" }
+quarkus-hibernate-orm = { module = "io.quarkus:quarkus-hibernate-orm" }
+quarkus-hibernate-validator = { module = "io.quarkus:quarkus-hibernate-validator" }
+quarkus-jdbc-h2 = { module = "io.quarkus:quarkus-jdbc-h2" }
+quarkus-jdbc-postgresql = { module = "io.quarkus:quarkus-jdbc-postgresql" }
+quarkus-flyway = { module = "io.quarkus:quarkus-flyway" }
+
+# Quarkus (Testing)
+quarkus-junit5-core = { module = "io.quarkus:quarkus-junit5" }
+quarkus-junit5-mockk = { module = "io.quarkiverse.mockk:quarkus-junit5-mockk", version.ref = "quarkus-junit5-mockk" }
+quarkus-jacoco = { module = "io.quarkus:quarkus-jacoco" }
+quarkus-test-security = { module = "io.quarkus:quarkus-test-security" }
+restassured-core = { module = "io.rest-assured:rest-assured" }
+restassured-kotlin = { module = "io.rest-assured:kotlin-extensions" }
# Other
classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph" }
+jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakarta-validation" }
hadoop-common = { module = "org.apache.hadoop:hadoop-common", version.ref = "hadoop" }
hadoop-mapreduce-client-core = { module = "org.apache.hadoop:hadoop-mapreduce-client-core", version.ref = "hadoop" }
commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" }
+microprofile-openapi-api = { module = "org.eclipse.microprofile.openapi:microprofile-openapi-api", version.ref = "microprofile-openapi" }
+
+# Other (Build)
+dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
+jandex-gradle = { module = "org.kordamp.gradle:jandex-gradle-plugin", version.ref = "jandex-gradle" }
+ktlint-gradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint-gradle" }
+jmh-gradle = { module = "me.champeau.jmh:jmh-gradle-plugin", version.ref = "jmh-gradle" }
+shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version.ref = "shadow" }
diff --git a/opendc-api-spec.yml b/opendc-api-spec.yml
deleted file mode 120000
index ca5a3b0c..00000000
--- a/opendc-api-spec.yml
+++ /dev/null
@@ -1 +0,0 @@
-opendc-web/opendc-web-api/static/schema.yml \ No newline at end of file
diff --git a/opendc-web/opendc-web-api/.coveragerc b/opendc-web/opendc-web-api/.coveragerc
deleted file mode 100644
index 55d99c2e..00000000
--- a/opendc-web/opendc-web-api/.coveragerc
+++ /dev/null
@@ -1,5 +0,0 @@
-[run]
-source = .
-omit =
- tests/*
- conftest.py
diff --git a/opendc-web/opendc-web-api/.dockerignore b/opendc-web/opendc-web-api/.dockerignore
deleted file mode 100644
index 06d67de9..00000000
--- a/opendc-web/opendc-web-api/.dockerignore
+++ /dev/null
@@ -1,8 +0,0 @@
-Dockerfile
-
-.idea/
-**/out
-*.iml
-.idea_modules/
-
-.pytest_cache
diff --git a/opendc-web/opendc-web-api/.gitignore b/opendc-web/opendc-web-api/.gitignore
deleted file mode 100644
index 9f8dfc5c..00000000
--- a/opendc-web/opendc-web-api/.gitignore
+++ /dev/null
@@ -1,19 +0,0 @@
-.DS_Store
-*.pyc
-*.pyo
-venv
-venv*
-dist
-build
-*.egg
-*.egg-info
-_mailinglist
-.tox
-.cache/
-.idea/
-config.json
-test.json
-.env*
-.coverage
-coverage.xml
-junit-report.xml
diff --git a/opendc-web/opendc-web-api/.pylintrc b/opendc-web/opendc-web-api/.pylintrc
deleted file mode 100644
index 4dbb0b50..00000000
--- a/opendc-web/opendc-web-api/.pylintrc
+++ /dev/null
@@ -1,523 +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,
- fixme,
- no-self-use
-
-# 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*(# )?<?https?://\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/opendc-web/opendc-web-api/.style.yapf b/opendc-web/opendc-web-api/.style.yapf
deleted file mode 100644
index f5c26c57..00000000
--- a/opendc-web/opendc-web-api/.style.yapf
+++ /dev/null
@@ -1,3 +0,0 @@
-[style]
-based_on_style = pep8
-column_limit=120
diff --git a/opendc-web/opendc-web-api/Dockerfile b/opendc-web/opendc-web-api/Dockerfile
index 505a69de..ff300170 100644
--- a/opendc-web/opendc-web-api/Dockerfile
+++ b/opendc-web/opendc-web-api/Dockerfile
@@ -1,23 +1,17 @@
-FROM python:3.9-slim
+FROM openjdk:17-slim
MAINTAINER OpenDC Maintainers <opendc@atlarge-research.com>
-# Ensure the STDOUT is not buffered by Python so that our logs become visible
-# See https://stackoverflow.com/q/29663459/10213073
-ENV PYTHONUNBUFFERED 1
+# Obtain (cache) Gradle wrapper
+COPY gradlew /app/
+COPY gradle /app/gradle
+WORKDIR /app
+RUN ./gradlew --version
-# Copy OpenDC directory
-COPY ./ /opendc
+# Build project
+COPY ./ /app/
+RUN ./gradlew --no-daemon :opendc-web:opendc-web-api:build
-# Fetch web server dependencies
-RUN pip install -r /opendc/requirements.txt && pip install pyuwsgi
-
-# Create opendc user
-RUN groupadd --gid 1000 opendc \
- && useradd --uid 1000 --gid opendc --shell /bin/bash --create-home opendc
-RUN chown -R opendc:opendc /opendc
-USER opendc
-
-# Set working directory
-WORKDIR /opendc
-
-CMD uwsgi -M --socket 0.0.0.0:80 --protocol=http --wsgi-file app.py --enable-threads --processes 2 --lazy-app
+FROM openjdk:17-slim
+COPY --from=0 /app/opendc-web/opendc-web-api/build/quarkus-app /opt/opendc
+WORKDIR /opt/opendc
+CMD java -jar quarkus-run.jar
diff --git a/opendc-web/opendc-web-api/README.md b/opendc-web/opendc-web-api/README.md
deleted file mode 100644
index d1c469c1..00000000
--- a/opendc-web/opendc-web-api/README.md
+++ /dev/null
@@ -1,122 +0,0 @@
-<h1 align="center">
- <img src="../../docs/images/logo.png" width="100" alt="OpenDC">
- <br>
- OpenDC Web Server
-</h1>
-<p align="center">
- Collaborative Datacenter Simulation and Exploration for Everybody
-</p>
-
-<br>
-
-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](docs/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 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.
-
-### 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 from the deployment guide](../../docs/deploy.md) to set up an [Auth0](https://auth0.com)
-application and environment variables.
-
-**Important:** Be sure to set up environment variables according to those instructions, in a `.env` file.
-
-#### Set up the database
-
-You can selectively run only the database services from the standard OpenDC `docker-compose` setup (in the root
-directory):
-
-```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. Add the simulator
-images to the command lists above if you want to test simulation capabilities, as well.
-
-### Local Development
-
-Run the server.
-
-```bash
-python3 -m flask run --port 8081
-```
-
-When editing the web server code, restart the server (`CTRL` + `c` followed by `python app.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. The script uses `yapf` internally to format everything
-automatically.
-
-To check if code style is up to modern standards, run `check.sh` in this directory. The script uses `pylint` internally.
-
-#### Testing
-
-Run `pytest opendc` in this directory to run all tests.
diff --git a/opendc-web/opendc-web-api/app.py b/opendc-web/opendc-web-api/app.py
deleted file mode 100755
index 36c80b7a..00000000
--- a/opendc-web/opendc-web-api/app.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env python3
-import mimetypes
-import os
-
-from dotenv import load_dotenv
-from flask import Flask, jsonify, redirect
-from flask_compress import Compress
-from flask_cors import CORS
-from flask_restful import Api
-from flask_swagger_ui import get_swaggerui_blueprint
-from marshmallow import ValidationError
-
-from opendc.api.jobs import JobList, Job
-from opendc.api.portfolios import Portfolio, PortfolioScenarios
-from opendc.api.prefabs import Prefab, PrefabList
-from opendc.api.projects import ProjectList, Project, ProjectTopologies, ProjectPortfolios
-from opendc.api.scenarios import Scenario
-from opendc.api.schedulers import SchedulerList
-from opendc.api.topologies import Topology
-from opendc.api.traces import TraceList, Trace
-from opendc.auth import AuthError
-from opendc.util import JSONEncoder
-
-
-# Load environmental variables from dotenv file
-load_dotenv()
-
-
-def setup_sentry():
- """
- Setup the Sentry integration for Flask if a DSN is supplied via the environmental variables.
- """
- if 'SENTRY_DSN' not in os.environ:
- return
-
- import sentry_sdk
- from sentry_sdk.integrations.flask import FlaskIntegration
-
- sentry_sdk.init(
- integrations=[FlaskIntegration()],
- traces_sample_rate=0.1
- )
-
-
-def setup_api(app):
- """
- Setup the API interface.
- """
- api = Api(app)
- # Map to ('string', 'ObjectId') passing type and format
- api.add_resource(ProjectList, '/projects/')
- api.add_resource(Project, '/projects/<string:project_id>')
- api.add_resource(ProjectTopologies, '/projects/<string:project_id>/topologies')
- api.add_resource(ProjectPortfolios, '/projects/<string:project_id>/portfolios')
- api.add_resource(Topology, '/topologies/<string:topology_id>')
- api.add_resource(PrefabList, '/prefabs/')
- api.add_resource(Prefab, '/prefabs/<string:prefab_id>')
- api.add_resource(Portfolio, '/portfolios/<string:portfolio_id>')
- api.add_resource(PortfolioScenarios, '/portfolios/<string:portfolio_id>/scenarios')
- api.add_resource(Scenario, '/scenarios/<string:scenario_id>')
- api.add_resource(TraceList, '/traces/')
- api.add_resource(Trace, '/traces/<string:trace_id>')
- api.add_resource(SchedulerList, '/schedulers/')
- api.add_resource(JobList, '/jobs/')
- api.add_resource(Job, '/jobs/<string:job_id>')
-
- @app.errorhandler(AuthError)
- def handle_auth_error(ex):
- response = jsonify(ex.error)
- response.status_code = ex.status_code
- return response
-
- @app.errorhandler(ValidationError)
- def handle_validation_error(ex):
- return {'message': 'Input validation failed', 'errors': ex.messages}, 400
-
- return api
-
-
-def setup_swagger(app):
- """
- Setup Swagger UI
- """
- SWAGGER_URL = '/docs'
- API_URL = '../schema.yml'
-
- swaggerui_blueprint = get_swaggerui_blueprint(
- SWAGGER_URL,
- API_URL,
- config={
- 'app_name': "OpenDC API v2"
- },
- oauth_config={
- 'clientId': os.environ.get("AUTH0_DOCS_CLIENT_ID", ""),
- 'additionalQueryStringParams': {'audience': os.environ.get("AUTH0_AUDIENCE", "https://api.opendc.org/v2/")},
- }
- )
- app.register_blueprint(swaggerui_blueprint)
-
-
-def create_app(testing=False):
- app = Flask(__name__, static_url_path='/')
- app.config['TESTING'] = testing
- app.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET']
- app.config['RESTFUL_JSON'] = {'cls': JSONEncoder}
- app.json_encoder = JSONEncoder
-
- # Define YAML content type
- mimetypes.add_type('text/yaml', '.yml')
-
- # Setup Sentry if DSN is specified
- setup_sentry()
-
- # Set up CORS support
- CORS(app)
-
- # Setup compression
- compress = Compress()
- compress.init_app(app)
-
- setup_api(app)
- setup_swagger(app)
-
- @app.route('/')
- def index():
- """
- Redirect the user to the API documentation if it accesses the API root.
- """
- return redirect('docs/')
-
- return app
-
-
-application = create_app(testing="OPENDC_FLASK_TESTING" in os.environ)
-
-if __name__ == '__main__':
- application.run()
diff --git a/opendc-web/opendc-web-api/build.gradle.kts b/opendc-web/opendc-web-api/build.gradle.kts
index 7edfd134..853632a7 100644
--- a/opendc-web/opendc-web-api/build.gradle.kts
+++ b/opendc-web/opendc-web-api/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * 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
@@ -19,3 +19,75 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
+
+description = "REST API for the OpenDC website"
+
+/* Build configuration */
+plugins {
+ `kotlin-conventions`
+ kotlin("plugin.allopen")
+ kotlin("plugin.jpa")
+ `testing-conventions`
+ `jacoco-conventions`
+ id("io.quarkus")
+}
+
+dependencies {
+ implementation(enforcedPlatform(libs.quarkus.bom))
+
+ implementation(projects.opendcWeb.opendcWebProto)
+
+ implementation(libs.quarkus.kotlin)
+ implementation(libs.quarkus.resteasy.core)
+ implementation(libs.quarkus.resteasy.jackson)
+ implementation(libs.jackson.module.kotlin)
+ implementation(libs.quarkus.smallrye.openapi)
+
+ implementation(libs.quarkus.security)
+ implementation(libs.quarkus.oidc)
+
+ implementation(libs.quarkus.hibernate.orm)
+ implementation(libs.quarkus.hibernate.validator)
+ implementation(libs.quarkus.jdbc.postgresql)
+ quarkusDev(libs.quarkus.jdbc.h2)
+
+ testImplementation(libs.quarkus.junit5.core)
+ testImplementation(libs.quarkus.junit5.mockk)
+ testImplementation(libs.quarkus.jacoco)
+ testImplementation(libs.restassured.core)
+ testImplementation(libs.restassured.kotlin)
+ testImplementation(libs.quarkus.test.security)
+ testImplementation(libs.quarkus.jdbc.h2)
+}
+
+allOpen {
+ annotation("javax.ws.rs.Path")
+ annotation("javax.enterprise.context.ApplicationScoped")
+ annotation("io.quarkus.test.junit.QuarkusTest")
+ annotation("javax.persistence.Entity")
+}
+
+tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
+ kotlinOptions.javaParameters = true
+}
+
+tasks.quarkusDev {
+ workingDir = rootProject.projectDir.toString()
+}
+
+tasks.test {
+ extensions.configure(JacocoTaskExtension::class) {
+ excludeClassLoaders = listOf("*QuarkusClassLoader")
+ // destinationFile = layout.buildDirectory.file("jacoco-quarkus.exec").get().asFile
+ }
+}
+
+/* Fix for Quarkus/ktlint-gradle incompatibilities */
+tasks.named("runKtlintCheckOverMainSourceSet").configure {
+ mustRunAfter(tasks.quarkusGenerateCode)
+ mustRunAfter(tasks.quarkusGenerateCodeDev)
+}
+
+tasks.named("runKtlintCheckOverTestSourceSet").configure {
+ mustRunAfter(tasks.quarkusGenerateCodeTests)
+}
diff --git a/opendc-web/opendc-web-api/check.sh b/opendc-web/opendc-web-api/check.sh
deleted file mode 100755
index abe2c596..00000000
--- a/opendc-web/opendc-web-api/check.sh
+++ /dev/null
@@ -1 +0,0 @@
-pylint opendc --ignore-patterns=test_.*?py
diff --git a/opendc-web/opendc-web-api/conftest.py b/opendc-web/opendc-web-api/conftest.py
deleted file mode 100644
index 958a5894..00000000
--- a/opendc-web/opendc-web-api/conftest.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""
-Configuration file for all unit tests.
-"""
-
-from functools import wraps
-import pytest
-from flask import _request_ctx_stack, g
-from opendc.database import Database
-
-
-def requires_auth_mock(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- _request_ctx_stack.top.current_user = {'sub': 'test'}
- return f(*args, **kwargs)
- return decorated_function
-
-
-def requires_scope_mock(required_scope):
- def decorator(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- return f(*args, **kwargs)
- return decorated_function
- return decorator
-
-
-@pytest.fixture
-def client():
- """Returns a Flask API client to interact with."""
-
- # Disable authorization for test API endpoints
- from opendc import exts
- exts.requires_auth = requires_auth_mock
- exts.requires_scope = requires_scope_mock
- exts.has_scope = lambda x: False
-
- from app import create_app
-
- app = create_app(testing=True)
-
- with app.app_context():
- g.db = Database()
- with app.test_client() as client:
- yield client
diff --git a/opendc-web/opendc-web-api/docs/component-diagram.png b/opendc-web/opendc-web-api/docs/component-diagram.png
deleted file mode 100644
index 91b26006..00000000
--- a/opendc-web/opendc-web-api/docs/component-diagram.png
+++ /dev/null
Binary files differ
diff --git a/opendc-web/opendc-web-api/format.sh b/opendc-web/opendc-web-api/format.sh
deleted file mode 100755
index 18cba452..00000000
--- a/opendc-web/opendc-web-api/format.sh
+++ /dev/null
@@ -1 +0,0 @@
-yapf **/*.py -i
diff --git a/opendc-web/opendc-web-api/opendc/__init__.py b/opendc-web/opendc-web-api/opendc/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/api/__init__.py b/opendc-web/opendc-web-api/opendc/api/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/api/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/api/jobs.py b/opendc-web/opendc-web-api/opendc/api/jobs.py
deleted file mode 100644
index 6fb0522b..00000000
--- a/opendc-web/opendc-web-api/opendc/api/jobs.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# Copyright (c) 2021 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.
-from flask import request
-from flask_restful import Resource
-from marshmallow import fields, Schema, validate
-from werkzeug.exceptions import BadRequest, Conflict
-
-from opendc.exts import requires_auth, requires_scope
-from opendc.models.scenario import Scenario
-
-
-def convert_to_job(scenario):
- """Convert a scenario to a job.
- """
- return JobSchema().dump({
- '_id': scenario['_id'],
- 'scenarioId': scenario['_id'],
- 'state': scenario['simulation']['state'],
- 'heartbeat': scenario['simulation'].get('heartbeat', None),
- 'results': scenario.get('results', {})
- })
-
-
-class JobSchema(Schema):
- """
- Schema representing a simulation job.
- """
- _id = fields.String(dump_only=True)
- scenarioId = fields.String(dump_only=True)
- state = fields.String(required=True,
- validate=validate.OneOf(["QUEUED", "CLAIMED", "RUNNING", "FINISHED", "FAILED"]))
- heartbeat = fields.DateTime()
- results = fields.Dict()
-
-
-class JobList(Resource):
- """
- Resource representing the list of available jobs.
- """
- method_decorators = [requires_auth, requires_scope('runner')]
-
- def get(self):
- """Get all available jobs."""
- jobs = Scenario.get_jobs()
- data = list(map(convert_to_job, jobs.obj))
- return {'data': data}
-
-
-class Job(Resource):
- """
- Resource representing a single job.
- """
- method_decorators = [requires_auth, requires_scope('runner')]
-
- def get(self, job_id):
- """Get the details of a single job."""
- job = Scenario.from_id(job_id)
- job.check_exists()
- data = convert_to_job(job.obj)
- return {'data': data}
-
- def post(self, job_id):
- """Update the details of a single job."""
- action = JobSchema(only=('state', 'results')).load(request.json)
-
- job = Scenario.from_id(job_id)
- job.check_exists()
-
- old_state = job.obj['simulation']['state']
- new_state = action['state']
-
- if old_state == new_state:
- data = job.update_state(new_state)
- elif (old_state, new_state) == ('QUEUED', 'CLAIMED'):
- data = job.update_state('CLAIMED')
- elif (old_state, new_state) == ('CLAIMED', 'RUNNING'):
- data = job.update_state('RUNNING')
- elif (old_state, new_state) == ('RUNNING', 'FINISHED'):
- data = job.update_state('FINISHED', results=action.get('results', None))
- elif old_state in ('CLAIMED', 'RUNNING') and new_state == 'FAILED':
- data = job.update_state('FAILED')
- else:
- raise BadRequest('Invalid state transition')
-
- if not data:
- raise Conflict('State conflict')
-
- return {'data': convert_to_job(data)}
diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py
deleted file mode 100644
index 4d8f54fd..00000000
--- a/opendc-web/opendc-web-api/opendc/api/portfolios.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# Copyright (c) 2021 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.
-
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.exts import requires_auth, current_user, has_scope
-from opendc.models.portfolio import Portfolio as PortfolioModel, PortfolioSchema
-from opendc.models.project import Project
-from opendc.models.scenario import ScenarioSchema, Scenario
-from opendc.models.topology import Topology
-
-
-class Portfolio(Resource):
- """
- Resource representing a portfolio.
- """
- method_decorators = [requires_auth]
-
- def get(self, portfolio_id):
- """
- Get a portfolio by identifier.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
-
- # Users with scope runner can access all portfolios
- if not has_scope('runner'):
- portfolio.check_user_access(current_user['sub'], False)
-
- data = PortfolioSchema().dump(portfolio.obj)
- return {'data': data}
-
- def put(self, portfolio_id):
- """
- Replace the portfolio.
- """
- schema = Portfolio.PutSchema()
- result = schema.load(request.json)
-
- portfolio = PortfolioModel.from_id(portfolio_id)
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- portfolio.set_property('name', result['portfolio']['name'])
- portfolio.set_property('targets.enabledMetrics', result['portfolio']['targets']['enabledMetrics'])
- portfolio.set_property('targets.repeatsPerScenario', result['portfolio']['targets']['repeatsPerScenario'])
-
- portfolio.update()
- data = PortfolioSchema().dump(portfolio.obj)
- return {'data': data}
-
- def delete(self, portfolio_id):
- """
- Delete a portfolio.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- portfolio_id = portfolio.get_id()
-
- project = Project.from_id(portfolio.obj['projectId'])
- project.check_exists()
- if portfolio_id in project.obj['portfolioIds']:
- project.obj['portfolioIds'].remove(portfolio_id)
- project.update()
-
- old_object = portfolio.delete()
- data = PortfolioSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a portfolio.
- """
- portfolio = fields.Nested(PortfolioSchema, required=True)
-
-
-class PortfolioScenarios(Resource):
- """
- Resource representing the scenarios of a portfolio.
- """
- method_decorators = [requires_auth]
-
- def get(self, portfolio_id):
- """
- Get all scenarios belonging to a portfolio.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- scenarios = Scenario.get_for_portfolio(portfolio_id)
-
- data = ScenarioSchema().dump(scenarios, many=True)
- return {'data': data}
-
- def post(self, portfolio_id):
- """
- Add a new scenario to this portfolio
- """
- schema = PortfolioScenarios.PostSchema()
- result = schema.load(request.json)
-
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- scenario = Scenario(result['scenario'])
-
- topology = Topology.from_id(scenario.obj['topology']['topologyId'])
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- scenario.set_property('portfolioId', portfolio.get_id())
- scenario.set_property('simulation', {'state': 'QUEUED'})
- scenario.set_property('topology.topologyId', topology.get_id())
-
- scenario.insert()
-
- portfolio.obj['scenarioIds'].append(scenario.get_id())
- portfolio.update()
- data = ScenarioSchema().dump(scenario.obj)
- return {'data': data}
-
- class PostSchema(Schema):
- """
- Schema for the POST operation on a portfolio's scenarios.
- """
- scenario = fields.Nested(ScenarioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/prefabs.py b/opendc-web/opendc-web-api/opendc/api/prefabs.py
deleted file mode 100644
index 730546ba..00000000
--- a/opendc-web/opendc-web-api/opendc/api/prefabs.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# Copyright (c) 2021 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.
-
-from datetime import datetime
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.prefab import Prefab as PrefabModel, PrefabSchema
-from opendc.exts import current_user, requires_auth, db
-
-
-class PrefabList(Resource):
- """
- Resource for the list of prefabs available to the user.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """
- Get the available prefabs for a user.
- """
- user_id = current_user['sub']
-
- own_prefabs = db.fetch_all({'authorId': user_id}, PrefabModel.collection_name)
- public_prefabs = db.fetch_all({'visibility': 'public'}, PrefabModel.collection_name)
-
- authorizations = {"authorizations": []}
- authorizations["authorizations"].append(own_prefabs)
- authorizations["authorizations"].append(public_prefabs)
- return {'data': authorizations}
-
- def post(self):
- """
- Create a new prefab.
- """
- schema = PrefabList.PostSchema()
- result = schema.load(request.json)
-
- prefab = PrefabModel(result['prefab'])
- prefab.set_property('datetimeCreated', datetime.now())
- prefab.set_property('datetimeLastEdited', datetime.now())
-
- user_id = current_user['sub']
- prefab.set_property('authorId', user_id)
-
- prefab.insert()
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- class PostSchema(Schema):
- """
- Schema for the POST operation on the prefab list.
- """
- prefab = fields.Nested(PrefabSchema, required=True)
-
-
-class Prefab(Resource):
- """
- Resource representing a single prefab.
- """
- method_decorators = [requires_auth]
-
- def get(self, prefab_id):
- """Get this Prefab."""
- prefab = PrefabModel.from_id(prefab_id)
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- def put(self, prefab_id):
- """Update a prefab's name and/or contents."""
-
- schema = Prefab.PutSchema()
- result = schema.load(request.json)
-
- prefab = PrefabModel.from_id(prefab_id)
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
-
- prefab.set_property('name', result['prefab']['name'])
- prefab.set_property('rack', result['prefab']['rack'])
- prefab.set_property('datetimeLastEdited', datetime.now())
- prefab.update()
-
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- def delete(self, prefab_id):
- """Delete this Prefab."""
- prefab = PrefabModel.from_id(prefab_id)
-
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
-
- old_object = prefab.delete()
-
- data = PrefabSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a prefab.
- """
- prefab = fields.Nested(PrefabSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/projects.py b/opendc-web/opendc-web-api/opendc/api/projects.py
deleted file mode 100644
index 2b47c12e..00000000
--- a/opendc-web/opendc-web-api/opendc/api/projects.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# Copyright (c) 2021 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.
-
-from datetime import datetime
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.portfolio import Portfolio, PortfolioSchema
-from opendc.models.topology import Topology, TopologySchema
-from opendc.models.project import Project as ProjectModel, ProjectSchema
-from opendc.exts import current_user, requires_auth
-
-
-class ProjectList(Resource):
- """
- Resource representing the list of projects available to a user.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get the authorized projects of the user"""
- user_id = current_user['sub']
- projects = ProjectModel.get_for_user(user_id)
- data = ProjectSchema().dump(projects, many=True)
- return {'data': data}
-
- def post(self):
- """Create a new project, and return that new project."""
- user_id = current_user['sub']
-
- schema = Project.PutSchema()
- result = schema.load(request.json)
-
- topology = Topology({'name': 'Default topology', 'rooms': []})
- topology.insert()
-
- project = ProjectModel(result['project'])
- project.set_property('datetimeCreated', datetime.now())
- project.set_property('datetimeLastEdited', datetime.now())
- project.set_property('topologyIds', [topology.get_id()])
- project.set_property('portfolioIds', [])
- project.set_property('authorizations', [{'userId': user_id, 'level': 'OWN'}])
- project.insert()
-
- topology.set_property('projectId', project.get_id())
- topology.update()
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
-
-class Project(Resource):
- """
- Resource representing a single project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get this Project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], False)
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
- def put(self, project_id):
- """Update a project's name."""
- schema = Project.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- project.set_property('name', result['project']['name'])
- project.set_property('datetimeLastEdited', datetime.now())
- project.update()
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
- def delete(self, project_id):
- """Delete this Project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], 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()
-
- old_object = project.delete()
- data = ProjectSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project.
- """
- project = fields.Nested(ProjectSchema, required=True)
-
-
-class ProjectTopologies(Resource):
- """
- Resource representing the topologies of a project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get all topologies belonging to the project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- topologies = Topology.get_for_project(project_id)
- data = TopologySchema().dump(topologies, many=True)
-
- return {'data': data}
-
- def post(self, project_id):
- """Add a new Topology to the specified project and return it"""
- schema = ProjectTopologies.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- topology = Topology({
- 'projectId': project.get_id(),
- 'name': result['topology']['name'],
- 'rooms': result['topology']['rooms'],
- })
-
- topology.insert()
-
- project.obj['topologyIds'].append(topology.get_id())
- project.set_property('datetimeLastEdited', datetime.now())
- project.update()
-
- data = TopologySchema().dump(topology.obj)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project topology.
- """
- topology = fields.Nested(TopologySchema, required=True)
-
-
-class ProjectPortfolios(Resource):
- """
- Resource representing the portfolios of a project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get all portfolios belonging to the project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- portfolios = Portfolio.get_for_project(project_id)
- data = PortfolioSchema().dump(portfolios, many=True)
-
- return {'data': data}
-
- def post(self, project_id):
- """Add a new Portfolio for this Project."""
- schema = ProjectPortfolios.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- portfolio = Portfolio(result['portfolio'])
-
- portfolio.set_property('projectId', project.get_id())
- portfolio.set_property('scenarioIds', [])
-
- portfolio.insert()
-
- project.obj['portfolioIds'].append(portfolio.get_id())
- project.update()
-
- data = PortfolioSchema().dump(portfolio.obj)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project portfolio.
- """
- portfolio = fields.Nested(PortfolioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/scenarios.py b/opendc-web/opendc-web-api/opendc/api/scenarios.py
deleted file mode 100644
index eacb0b49..00000000
--- a/opendc-web/opendc-web-api/opendc/api/scenarios.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (c) 2021 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.
-
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.scenario import Scenario as ScenarioModel, ScenarioSchema
-from opendc.models.portfolio import Portfolio
-from opendc.exts import current_user, requires_auth, has_scope
-
-
-class Scenario(Resource):
- """
- A Scenario resource.
- """
- method_decorators = [requires_auth]
-
- def get(self, scenario_id):
- """Get scenario by identifier."""
- scenario = ScenarioModel.from_id(scenario_id)
- scenario.check_exists()
-
- # Users with scope runner can access all scenarios
- if not has_scope('runner'):
- scenario.check_user_access(current_user['sub'], False)
-
- data = ScenarioSchema().dump(scenario.obj)
- return {'data': data}
-
- def put(self, scenario_id):
- """Update this Scenarios name."""
- schema = Scenario.PutSchema()
- result = schema.load(request.json)
-
- scenario = ScenarioModel.from_id(scenario_id)
-
- scenario.check_exists()
- scenario.check_user_access(current_user['sub'], True)
-
- scenario.set_property('name', result['scenario']['name'])
-
- scenario.update()
- data = ScenarioSchema().dump(scenario.obj)
- return {'data': data}
-
- def delete(self, scenario_id):
- """Delete this Scenario."""
- scenario = ScenarioModel.from_id(scenario_id)
- scenario.check_exists()
- scenario.check_user_access(current_user['sub'], True)
-
- scenario_id = scenario.get_id()
-
- portfolio = Portfolio.from_id(scenario.obj['portfolioId'])
- portfolio.check_exists()
- if scenario_id in portfolio.obj['scenarioIds']:
- portfolio.obj['scenarioIds'].remove(scenario_id)
- portfolio.update()
-
- old_object = scenario.delete()
- data = ScenarioSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the put operation.
- """
- scenario = fields.Nested(ScenarioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/schedulers.py b/opendc-web/opendc-web-api/opendc/api/schedulers.py
deleted file mode 100644
index b00d8c31..00000000
--- a/opendc-web/opendc-web-api/opendc/api/schedulers.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (c) 2021 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.
-
-
-from flask_restful import Resource
-from opendc.exts import requires_auth
-
-SCHEDULERS = [
- 'mem',
- 'mem-inv',
- 'core-mem',
- 'core-mem-inv',
- 'active-servers',
- 'active-servers-inv',
- 'provisioned-cores',
- 'provisioned-cores-inv',
- 'random'
-]
-
-
-class SchedulerList(Resource):
- """
- Resource for the list of schedulers to pick from.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get all available Traces."""
- return {'data': [{'name': name} for name in SCHEDULERS]}
diff --git a/opendc-web/opendc-web-api/opendc/api/topologies.py b/opendc-web/opendc-web-api/opendc/api/topologies.py
deleted file mode 100644
index c0b2e7ee..00000000
--- a/opendc-web/opendc-web-api/opendc/api/topologies.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# Copyright (c) 2021 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.
-
-from datetime import datetime
-
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.project import Project
-from opendc.models.topology import Topology as TopologyModel, TopologySchema
-from opendc.exts import current_user, requires_auth, has_scope
-
-
-class Topology(Resource):
- """
- Resource representing a single topology.
- """
- method_decorators = [requires_auth]
-
- def get(self, topology_id):
- """
- Get a single topology.
- """
- topology = TopologyModel.from_id(topology_id)
- topology.check_exists()
-
- # Users with scope runner can access all topologies
- if not has_scope('runner'):
- topology.check_user_access(current_user['sub'], False)
-
- data = TopologySchema().dump(topology.obj)
- return {'data': data}
-
- def put(self, topology_id):
- """
- Replace the topology.
- """
- topology = TopologyModel.from_id(topology_id)
-
- schema = Topology.PutSchema()
- result = schema.load(request.json)
-
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- topology.set_property('name', result['topology']['name'])
- topology.set_property('rooms', result['topology']['rooms'])
- topology.set_property('datetimeLastEdited', datetime.now())
-
- topology.update()
- data = TopologySchema().dump(topology.obj)
- return {'data': data}
-
- def delete(self, topology_id):
- """
- Delete a topology.
- """
- topology = TopologyModel.from_id(topology_id)
-
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- topology_id = topology.get_id()
-
- project = Project.from_id(topology.obj['projectId'])
- project.check_exists()
- if topology_id in project.obj['topologyIds']:
- project.obj['topologyIds'].remove(topology_id)
- project.update()
-
- old_object = topology.delete()
- data = TopologySchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a topology.
- """
- topology = fields.Nested(TopologySchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/traces.py b/opendc-web/opendc-web-api/opendc/api/traces.py
deleted file mode 100644
index 6be8c5e5..00000000
--- a/opendc-web/opendc-web-api/opendc/api/traces.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (c) 2021 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.
-
-from flask_restful import Resource
-
-from opendc.exts import requires_auth
-from opendc.models.trace import Trace as TraceModel, TraceSchema
-
-
-class TraceList(Resource):
- """
- Resource for the list of traces to pick from.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get all available Traces."""
- traces = TraceModel.get_all()
- data = TraceSchema().dump(traces.obj, many=True)
- return {'data': data}
-
-
-class Trace(Resource):
- """
- Resource representing a single trace.
- """
- method_decorators = [requires_auth]
-
- def get(self, trace_id):
- """Get trace information by identifier."""
- trace = TraceModel.from_id(trace_id)
- trace.check_exists()
- data = TraceSchema().dump(trace.obj)
- return {'data': data}
diff --git a/opendc-web/opendc-web-api/opendc/auth.py b/opendc-web/opendc-web-api/opendc/auth.py
deleted file mode 100644
index d5da6ee5..00000000
--- a/opendc-web/opendc-web-api/opendc/auth.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# Copyright (c) 2021 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.
-
-import json
-import time
-
-import urllib3
-from flask import request
-from jose import jwt, JWTError
-
-
-def get_token():
- """
- Obtain the Access Token from the Authorization Header
- """
- auth = request.headers.get("Authorization", None)
- if not auth:
- raise AuthError({
- "code": "authorization_header_missing",
- "description": "Authorization header is expected"
- }, 401)
-
- parts = auth.split()
-
- if parts[0].lower() != "bearer":
- raise AuthError({"code": "invalid_header", "description": "Authorization header must start with Bearer"}, 401)
- if len(parts) == 1:
- raise AuthError({"code": "invalid_header", "description": "Token not found"}, 401)
- if len(parts) > 2:
- raise AuthError({"code": "invalid_header", "description": "Authorization header must be" " Bearer token"}, 401)
-
- token = parts[1]
- return token
-
-
-class AuthError(Exception):
- """
- This error is thrown when the request failed to authorize.
- """
- def __init__(self, error, status_code):
- Exception.__init__(self, error)
- self.error = error
- self.status_code = status_code
-
-
-class AuthContext:
- """
- This class handles the authorization of requests.
- """
- def __init__(self, alg, issuer, audience):
- self._alg = alg
- self._issuer = issuer
- self._audience = audience
-
- def validate(self, token):
- """
- Validate the specified JWT token.
- :param token: The authorization token specified by the user.
- :return: The token payload on success, otherwise `AuthError`.
- """
- try:
- header = jwt.get_unverified_header(token)
- except JWTError as e:
- raise AuthError({"code": "invalid_token", "message": str(e)}, 401)
-
- alg = header.get('alg', None)
- if alg != self._alg.algorithm:
- raise AuthError(
- {
- "code":
- "invalid_header",
- "message":
- f"Signature algorithm of {alg} is not supported. Expected the ID token "
- f"to be signed with {self._alg.algorithm}"
- }, 401)
-
- kid = header.get('kid', None)
- try:
- secret_or_certificate = self._alg.get_key(key_id=kid)
- except TokenValidationError as e:
- raise AuthError({"code": "invalid_header", "message": str(e)}, 401)
- try:
- payload = jwt.decode(token,
- key=secret_or_certificate,
- algorithms=[self._alg.algorithm],
- audience=self._audience,
- issuer=self._issuer)
- return payload
- except jwt.ExpiredSignatureError:
- raise AuthError({"code": "token_expired", "message": "Token is expired"}, 401)
- except jwt.JWTClaimsError:
- raise AuthError(
- {
- "code": "invalid_claims",
- "message": "Incorrect claims, please check the audience and issuer"
- }, 401)
- except Exception as e:
- print(e)
- raise AuthError({"code": "invalid_header", "message": "Unable to parse authentication token."}, 401)
-
-
-class SymmetricJwtAlgorithm:
- """Verifier for HMAC signatures, which rely on shared secrets.
- Args:
- shared_secret (str): The shared secret used to decode the token.
- algorithm (str, optional): The expected signing algorithm. Defaults to "HS256".
- """
- def __init__(self, shared_secret, algorithm="HS256"):
- self.algorithm = algorithm
- self._shared_secret = shared_secret
-
- # pylint: disable=W0613
- def get_key(self, key_id=None):
- """
- Obtain the key for this algorithm.
- :param key_id: The identifier of the key.
- :return: The JWK key.
- """
- return self._shared_secret
-
-
-class AsymmetricJwtAlgorithm:
- """Verifier for RSA signatures, which rely on public key certificates.
- Args:
- jwks_url (str): The url where the JWK set is located.
- algorithm (str, optional): The expected signing algorithm. Defaults to "RS256".
- """
- def __init__(self, jwks_url, algorithm="RS256"):
- self.algorithm = algorithm
- self._fetcher = JwksFetcher(jwks_url)
-
- def get_key(self, key_id=None):
- """
- Obtain the key for this algorithm.
- :param key_id: The identifier of the key.
- :return: The JWK key.
- """
- return self._fetcher.get_key(key_id)
-
-
-class TokenValidationError(Exception):
- """
- Error thrown when the token cannot be validated
- """
-
-
-class JwksFetcher:
- """Class that fetches and holds a JSON web key set.
- This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it.
- Args:
- jwks_url (str): The url where the JWK set is located.
- cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds.
- """
- CACHE_TTL = 600 # 10 min cache lifetime
-
- def __init__(self, jwks_url, cache_ttl=CACHE_TTL):
- self._jwks_url = jwks_url
- self._http = urllib3.PoolManager()
- self._cache_value = {}
- self._cache_date = 0
- self._cache_ttl = cache_ttl
- self._cache_is_fresh = False
-
- def _fetch_jwks(self, force=False):
- """Attempts to obtain the JWK set from the cache, as long as it's still valid.
- When not, it will perform a network request to the jwks_url to obtain a fresh result
- and update the cache value with it.
- Args:
- force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False.
- """
- has_expired = self._cache_date + self._cache_ttl < time.time()
-
- if not force and not has_expired:
- # Return from cache
- self._cache_is_fresh = False
- return self._cache_value
-
- # Invalidate cache and fetch fresh data
- self._cache_value = {}
- response = self._http.request('GET', self._jwks_url)
-
- if response.status == 200:
- # Update cache
- jwks = json.loads(response.data.decode('utf-8'))
- self._cache_value = self._parse_jwks(jwks)
- self._cache_is_fresh = True
- self._cache_date = time.time()
- return self._cache_value
-
- @staticmethod
- def _parse_jwks(jwks):
- """Converts a JWK string representation into a binary certificate in PEM format.
- """
- keys = {}
-
- for key in jwks['keys']:
- keys[key["kid"]] = key
- return keys
-
- def get_key(self, key_id):
- """Obtains the JWK associated with the given key id.
- Args:
- key_id (str): The id of the key to fetch.
- Returns:
- the JWK associated with the given key id.
-
- Raises:
- TokenValidationError: when a key with that id cannot be found
- """
- keys = self._fetch_jwks()
-
- if keys and key_id in keys:
- return keys[key_id]
-
- if not self._cache_is_fresh:
- keys = self._fetch_jwks(force=True)
- if keys and key_id in keys:
- return keys[key_id]
- raise TokenValidationError(f"RSA Public Key with ID {key_id} was not found.")
diff --git a/opendc-web/opendc-web-api/opendc/database.py b/opendc-web/opendc-web-api/opendc/database.py
deleted file mode 100644
index dd6367f2..00000000
--- a/opendc-web/opendc-web-api/opendc/database.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# Copyright (c) 2021 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.
-
-import urllib.parse
-
-from pymongo import MongoClient, ReturnDocument
-
-DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S'
-CONNECTION_POOL = None
-
-
-class Database:
- """Object holding functionality for database access."""
- def __init__(self, db=None):
- """Initializes the database connection."""
- self.opendc_db = db
-
- @classmethod
- def from_credentials(cls, user, password, database, host):
- """
- Construct a database instance from the specified credentials.
- :param user: The username to connect with.
- :param password: The password to connect with.
- :param database: The database name to connect to.
- :param host: The host to connect to.
- :return: The database instance.
- """
- 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))
- return cls(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}`.
- """
- return getattr(self.opendc_db, collection).find_one(query)
-
- 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}`.
- """
- cursor = getattr(self.opendc_db, collection).find(query)
- return list(cursor)
-
- def insert(self, obj, collection):
- """Updates an existing object."""
- bson = getattr(self.opendc_db, collection).insert(obj)
-
- return bson
-
- def update(self, _id, obj, collection):
- """Updates an existing object."""
- return getattr(self.opendc_db, collection).update({'_id': _id}, obj)
-
- def fetch_and_update(self, query, update, collection):
- """Updates an existing object."""
- return getattr(self.opendc_db, collection).find_one_and_update(query,
- update,
- return_document=ReturnDocument.AFTER)
-
- 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)
diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py
deleted file mode 100644
index 3ee8babb..00000000
--- a/opendc-web/opendc-web-api/opendc/exts.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import os
-from functools import wraps
-
-from flask import g, _request_ctx_stack
-from jose import jwt
-from werkzeug.local import LocalProxy
-
-from opendc.database import Database
-from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token, AuthError
-
-
-def get_db():
- """
- Return the configured database instance for the application.
- """
- _db = getattr(g, 'db', None)
- if _db is None:
- _db = Database.from_credentials(user=os.environ['OPENDC_DB_USERNAME'],
- password=os.environ['OPENDC_DB_PASSWORD'],
- database=os.environ['OPENDC_DB'],
- host=os.environ.get('OPENDC_DB_HOST', 'localhost'))
- g.db = _db
- return _db
-
-
-db = LocalProxy(get_db)
-
-
-def get_auth_context():
- """
- Return the configured auth context for the application.
- """
- _auth_context = getattr(g, 'auth_context', None)
- if _auth_context is None:
- _auth_context = AuthContext(
- alg=AsymmetricJwtAlgorithm(jwks_url=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json"),
- issuer=f"https://{os.environ['AUTH0_DOMAIN']}/",
- audience=os.environ['AUTH0_AUDIENCE'])
- g.auth_context = _auth_context
- return _auth_context
-
-
-auth_context = LocalProxy(get_auth_context)
-
-
-def requires_auth(f):
- """Decorator to determine if the Access Token is valid.
- """
- @wraps(f)
- def decorated(*args, **kwargs):
- token = get_token()
- payload = auth_context.validate(token)
- _request_ctx_stack.top.current_user = payload
- return f(*args, **kwargs)
-
- return decorated
-
-
-current_user = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user', None))
-
-
-def has_scope(required_scope):
- """Determines if the required scope is present in the Access Token
- Args:
- required_scope (str): The scope required to access the resource
- """
- token = get_token()
- unverified_claims = jwt.get_unverified_claims(token)
- if unverified_claims.get("scope"):
- token_scopes = unverified_claims["scope"].split()
- for token_scope in token_scopes:
- if token_scope == required_scope:
- return True
- return False
-
-
-def requires_scope(required_scope):
- """Determines if the required scope is present in the Access Token
- Args:
- required_scope (str): The scope required to access the resource
- """
- def decorator(f):
- @wraps(f)
- def decorated(*args, **kwargs):
- if not has_scope(required_scope):
- raise AuthError({"code": "Unauthorized", "description": "You don't have access to this resource"}, 403)
- return f(*args, **kwargs)
-
- return decorated
-
- return decorator
diff --git a/opendc-web/opendc-web-api/opendc/models/__init__.py b/opendc-web/opendc-web-api/opendc/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/models/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/models/model.py b/opendc-web/opendc-web-api/opendc/models/model.py
deleted file mode 100644
index 28299453..00000000
--- a/opendc-web/opendc-web-api/opendc/models/model.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from bson.objectid import ObjectId
-from werkzeug.exceptions import NotFound
-
-from opendc.exts import db
-
-
-class Model:
- """Base class for all models."""
-
- collection_name = '<specified in subclasses>'
-
- @classmethod
- def from_id(cls, _id):
- """Fetches the document with given ID from the collection."""
- if isinstance(_id, str) and len(_id) == 24:
- _id = ObjectId(_id)
-
- 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 self.obj['_id']
-
- def check_exists(self):
- """Raises an error if the enclosed object does not exist."""
- if self.obj is None:
- raise NotFound('Entity 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'] = ObjectId()
- 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/opendc-web/opendc-web-api/opendc/models/portfolio.py b/opendc-web/opendc-web-api/opendc/models/portfolio.py
deleted file mode 100644
index eb016947..00000000
--- a/opendc-web/opendc-web-api/opendc/models/portfolio.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.project import Project
-from opendc.models.model import Model
-
-
-class TargetSchema(Schema):
- """
- Schema representing a target.
- """
- enabledMetrics = fields.List(fields.String())
- repeatsPerScenario = fields.Integer(required=True)
-
-
-class PortfolioSchema(Schema):
- """
- Schema representing a portfolio.
- """
- _id = fields.String(dump_only=True)
- projectId = fields.String()
- name = fields.String(required=True)
- scenarioIds = fields.List(fields.String())
- targets = fields.Nested(TargetSchema)
-
-
-class Portfolio(Model):
- """Model representing a Portfolio."""
-
- collection_name = 'portfolios'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- project = Project.from_id(self.obj['projectId'])
- project.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_for_project(cls, project_id):
- """Get all portfolios for the specified project id."""
- return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/prefab.py b/opendc-web/opendc-web-api/opendc/models/prefab.py
deleted file mode 100644
index 5e4b81dc..00000000
--- a/opendc-web/opendc-web-api/opendc/models/prefab.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from marshmallow import Schema, fields
-from werkzeug.exceptions import Forbidden
-
-from opendc.models.topology import ObjectSchema
-from opendc.models.model import Model
-
-
-class PrefabSchema(Schema):
- """
- Schema for a Prefab.
- """
- _id = fields.String(dump_only=True)
- authorId = fields.String(dump_only=True)
- name = fields.String(required=True)
- datetimeCreated = fields.DateTime()
- datetimeLastEdited = fields.DateTime()
- rack = fields.Nested(ObjectSchema)
-
-
-class Prefab(Model):
- """Model representing a Prefab."""
-
- collection_name = 'prefabs'
-
- def check_user_access(self, user_id):
- """Raises an error if the user with given [user_id] has insufficient access to view this prefab.
-
- :param user_id: The user ID of the user.
- """
- if self.obj['authorId'] != user_id and self.obj['visibility'] == "private":
- raise Forbidden("Forbidden from retrieving prefab.")
diff --git a/opendc-web/opendc-web-api/opendc/models/project.py b/opendc-web/opendc-web-api/opendc/models/project.py
deleted file mode 100644
index f2b3b564..00000000
--- a/opendc-web/opendc-web-api/opendc/models/project.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from marshmallow import Schema, fields, validate
-from werkzeug.exceptions import Forbidden
-
-from opendc.models.model import Model
-from opendc.exts import db
-
-
-class ProjectAuthorizations(Schema):
- """
- Schema representing a project authorization.
- """
- userId = fields.String(required=True)
- level = fields.String(required=True, validate=validate.OneOf(["VIEW", "EDIT", "OWN"]))
-
-
-class ProjectSchema(Schema):
- """
- Schema representing a Project.
- """
- _id = fields.String(dump_only=True)
- name = fields.String(required=True)
- datetimeCreated = fields.DateTime()
- datetimeLastEdited = fields.DateTime()
- topologyIds = fields.List(fields.String())
- portfolioIds = fields.List(fields.String())
- authorizations = fields.List(fields.Nested(ProjectAuthorizations))
-
-
-class Project(Model):
- """Model representing a Project."""
-
- collection_name = 'projects'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- for authorization in self.obj['authorizations']:
- if user_id == authorization['userId'] and authorization['level'] != 'VIEW' or not edit_access:
- return
- raise Forbidden("Forbidden from retrieving project.")
-
- @classmethod
- def get_for_user(cls, user_id):
- """Get all projects for the specified user id."""
- return db.fetch_all({'authorizations.userId': user_id}, Project.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/scenario.py b/opendc-web/opendc-web-api/opendc/models/scenario.py
deleted file mode 100644
index 47771e06..00000000
--- a/opendc-web/opendc-web-api/opendc/models/scenario.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from datetime import datetime
-
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.model import Model
-from opendc.models.portfolio import Portfolio
-
-
-class SimulationSchema(Schema):
- """
- Simulation details.
- """
- state = fields.String()
-
-
-class TraceSchema(Schema):
- """
- Schema for specifying the trace of a scenario.
- """
- traceId = fields.String()
- loadSamplingFraction = fields.Float()
-
-
-class TopologySchema(Schema):
- """
- Schema for topology specification for a scenario.
- """
- topologyId = fields.String()
-
-
-class OperationalSchema(Schema):
- """
- Schema for the operational phenomena for a scenario.
- """
- failuresEnabled = fields.Boolean()
- performanceInterferenceEnabled = fields.Boolean()
- schedulerName = fields.String()
-
-
-class ScenarioSchema(Schema):
- """
- Schema representing a scenario.
- """
- _id = fields.String(dump_only=True)
- portfolioId = fields.String()
- name = fields.String(required=True)
- trace = fields.Nested(TraceSchema)
- topology = fields.Nested(TopologySchema)
- operational = fields.Nested(OperationalSchema)
- simulation = fields.Nested(SimulationSchema, dump_only=True)
- results = fields.Dict(dump_only=True)
-
-
-class Scenario(Model):
- """Model representing a Scenario."""
-
- collection_name = 'scenarios'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- portfolio = Portfolio.from_id(self.obj['portfolioId'])
- portfolio.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_jobs(cls):
- """Obtain the scenarios that have been queued.
- """
- return cls(db.fetch_all({'simulation.state': 'QUEUED'}, cls.collection_name))
-
- @classmethod
- def get_for_portfolio(cls, portfolio_id):
- """Get all scenarios for the specified portfolio id."""
- return db.fetch_all({'portfolioId': ObjectId(portfolio_id)}, cls.collection_name)
-
- def update_state(self, new_state, results=None):
- """Atomically update the state of the Scenario.
- """
- update = {'$set': {'simulation.state': new_state, 'simulation.heartbeat': datetime.now()}}
- if results:
- update['$set']['results'] = results
- return db.fetch_and_update(
- query={'_id': self.obj['_id'], 'simulation.state': self.obj['simulation']['state']},
- update=update,
- collection=self.collection_name
- )
diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py
deleted file mode 100644
index 44994818..00000000
--- a/opendc-web/opendc-web-api/opendc/models/topology.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.project import Project
-from opendc.models.model import Model
-
-
-class MemorySchema(Schema):
- """
- Schema representing a memory unit.
- """
- _id = fields.String()
- name = fields.String()
- speedMbPerS = fields.Integer()
- sizeMb = fields.Integer()
- energyConsumptionW = fields.Integer()
-
-
-class PuSchema(Schema):
- """
- Schema representing a processing unit.
- """
- _id = fields.String()
- name = fields.String()
- clockRateMhz = fields.Integer()
- numberOfCores = fields.Integer()
- energyConsumptionW = fields.Integer()
-
-
-class MachineSchema(Schema):
- """
- Schema representing a machine.
- """
- _id = fields.String()
- position = fields.Integer()
- cpus = fields.List(fields.Nested(PuSchema))
- gpus = fields.List(fields.Nested(PuSchema))
- memories = fields.List(fields.Nested(MemorySchema))
- storages = fields.List(fields.Nested(MemorySchema))
- rackId = fields.String()
-
-
-class ObjectSchema(Schema):
- """
- Schema representing a room object.
- """
- _id = fields.String()
- name = fields.String()
- capacity = fields.Integer()
- powerCapacityW = fields.Integer()
- machines = fields.List(fields.Nested(MachineSchema))
- tileId = fields.String()
-
-
-class TileSchema(Schema):
- """
- Schema representing a room tile.
- """
- _id = fields.String()
- topologyId = fields.String()
- positionX = fields.Integer()
- positionY = fields.Integer()
- rack = fields.Nested(ObjectSchema)
- roomId = fields.String()
-
-
-class RoomSchema(Schema):
- """
- Schema representing a room.
- """
- _id = fields.String()
- name = fields.String(required=True)
- topologyId = fields.String()
- tiles = fields.List(fields.Nested(TileSchema), required=True)
-
-
-class TopologySchema(Schema):
- """
- Schema representing a datacenter topology.
- """
- _id = fields.String(dump_only=True)
- projectId = fields.String()
- name = fields.String(required=True)
- rooms = fields.List(fields.Nested(RoomSchema), required=True)
- datetimeLastEdited = fields.DateTime()
-
-
-class Topology(Model):
- """Model representing a Project."""
-
- collection_name = 'topologies'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- project = Project.from_id(self.obj['projectId'])
- project.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_for_project(cls, project_id):
- """Get all topologies for the specified project id."""
- return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/trace.py b/opendc-web/opendc-web-api/opendc/models/trace.py
deleted file mode 100644
index 69287f29..00000000
--- a/opendc-web/opendc-web-api/opendc/models/trace.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from marshmallow import Schema, fields
-
-from opendc.models.model import Model
-
-
-class TraceSchema(Schema):
- """Schema for a Trace."""
- _id = fields.String(dump_only=True)
- name = fields.String()
- type = fields.String()
-
-
-class Trace(Model):
- """Model representing a Trace."""
-
- collection_name = 'traces'
diff --git a/opendc-web/opendc-web-api/opendc/util.py b/opendc-web/opendc-web-api/opendc/util.py
deleted file mode 100644
index e7dc07a4..00000000
--- a/opendc-web/opendc-web-api/opendc/util.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) 2021 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.
-
-import flask
-from bson.objectid import ObjectId
-
-
-class JSONEncoder(flask.json.JSONEncoder):
- """
- A customized JSON encoder to handle unsupported types.
- """
- def default(self, o):
- if isinstance(o, ObjectId):
- return str(o)
- return flask.json.JSONEncoder.default(self, o)
diff --git a/opendc-web/opendc-web-api/pytest.ini b/opendc-web/opendc-web-api/pytest.ini
deleted file mode 100644
index 8e7964ba..00000000
--- a/opendc-web/opendc-web-api/pytest.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[pytest]
-env =
- OPENDC_FLASK_TESTING=True
- OPENDC_FLASK_SECRET=Secret
-junit_family = xunit2
diff --git a/opendc-web/opendc-web-api/requirements.txt b/opendc-web/opendc-web-api/requirements.txt
deleted file mode 100644
index 6f3b42aa..00000000
--- a/opendc-web/opendc-web-api/requirements.txt
+++ /dev/null
@@ -1,47 +0,0 @@
-astroid==2.4.2
-attrs==20.3.0
-blinker==1.4
-Brotli==1.0.9
-certifi==2020.11.8
-click==7.1.2
-eventlet==0.31.0
-Flask==1.1.2
-Flask-Compress==1.5.0
-Flask-Cors==3.0.9
-Flask-SocketIO==4.3.1
-flask-swagger-ui==3.36.0
-Flask-Restful==0.3.8
-httplib2==0.19.0
-isort==4.3.21
-itsdangerous==1.1.0
-Jinja2==2.11.3
-lazy-object-proxy==1.4.3
-MarkupSafe==1.1.1
-marshmallow==3.12.1
-mccabe==0.6.1
-monotonic==1.5
-more-itertools==8.6.0
-oauth2client==4.1.3
-packaging==20.4
-pluggy==0.13.1
-py==1.10.0
-pyasn1==0.4.8
-pyasn1-modules==0.2.8
-pylint==2.5.3
-pymongo==3.10.1
-pyparsing==2.4.7
-pytest==5.4.3
-pytest-cov==2.11.1
-pytest-env==0.6.2
-pytest-mock==3.2.0
-python-dotenv==0.14.0
-python-jose==3.2.0
-rsa==4.7
-sentry-sdk==0.19.2
-six==1.15.0
-toml==0.10.2
-urllib3==1.26.5
-wcwidth==0.2.5
-Werkzeug==1.0.1
-wrapt==1.12.1
-yapf==0.30.0
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/SimulationState.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt
index 2eadd747..ddbd5390 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/SimulationState.kt
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt
@@ -20,11 +20,11 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.api
+
+import javax.ws.rs.core.Application
/**
- * The state of a simulation job.
+ * [Application] definition for the OpenDC web API.
*/
-public enum class SimulationState {
- QUEUED, CLAIMED, RUNNING, FINISHED, FAILED
-}
+class OpenDCApplication : Application()
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt
new file mode 100644
index 00000000..b09b46a1
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
+import org.opendc.web.proto.JobState
+import java.time.Instant
+import javax.persistence.*
+
+/**
+ * A simulation job to be run by the simulator.
+ */
+@TypeDef(name = "json", typeClass = JsonType::class)
+@Entity
+@Table(name = "jobs")
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Job.findAll",
+ query = "SELECT j FROM Job j WHERE j.state = :state"
+ ),
+ NamedQuery(
+ name = "Job.updateOne",
+ query = """
+ UPDATE Job j
+ SET j.state = :newState, j.updatedAt = :updatedAt, j.results = :results
+ WHERE j.id = :id AND j.state = :oldState
+ """
+ )
+ ]
+)
+class Job(
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ val id: Long,
+
+ @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER)
+ @JoinColumn(name = "scenario_id", nullable = false)
+ val scenario: Scenario,
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ val createdAt: Instant,
+
+ /**
+ * The number of simulation runs to perform.
+ */
+ @Column(nullable = false, updatable = false)
+ val repeats: Int
+) {
+ /**
+ * The instant at which the job was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ var updatedAt: Instant = createdAt
+
+ /**
+ * The state of the job.
+ */
+ @Column(nullable = false)
+ var state: JobState = JobState.PENDING
+
+ /**
+ * Experiment results in JSON
+ */
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb")
+ var results: Map<String, Any>? = null
+
+ /**
+ * Return a string representation of this job.
+ */
+ override fun toString(): String = "Job[id=$id,scenario=${scenario.id},state=$state]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt
new file mode 100644
index 00000000..c8b94daf
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
+import org.opendc.web.proto.Targets
+import javax.persistence.*
+
+/**
+ * A portfolio is the composition of multiple scenarios.
+ */
+@TypeDef(name = "json", typeClass = JsonType::class)
+@Entity
+@Table(
+ name = "portfolios",
+ uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
+ indexes = [Index(name = "fn_portfolios_number", columnList = "project_id, number")]
+)
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Portfolio.findAll",
+ query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"
+ ),
+ NamedQuery(
+ name = "Portfolio.findOne",
+ query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number"
+ )
+ ]
+)
+class Portfolio(
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ val id: Long,
+
+ /**
+ * Unique number of the portfolio for the project.
+ */
+ @Column(nullable = false)
+ val number: Int,
+
+ @Column(nullable = false)
+ val name: String,
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "project_id", nullable = false)
+ val project: Project,
+
+ /**
+ * The portfolio targets (metrics, repetitions).
+ */
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
+ val targets: Targets,
+) {
+ /**
+ * The scenarios in this portfolio.
+ */
+ @OneToMany(cascade = [CascadeType.ALL], mappedBy = "portfolio", orphanRemoval = true)
+ @OrderBy("id ASC")
+ val scenarios: MutableSet<Scenario> = mutableSetOf()
+
+ /**
+ * Return a string representation of this portfolio.
+ */
+ override fun toString(): String = "Job[id=$id,name=$name,project=${project.id},targets=$targets]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt
new file mode 100644
index 00000000..e0440bf4
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import java.time.Instant
+import javax.persistence.*
+
+/**
+ * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users.
+ */
+@Entity
+@Table(name = "projects")
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Project.findAll",
+ query = """
+ SELECT a
+ FROM ProjectAuthorization a
+ WHERE a.key.userId = :userId
+ """
+ ),
+ NamedQuery(
+ name = "Project.allocatePortfolio",
+ query = """
+ UPDATE Project p
+ SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.portfoliosCreated = :oldState
+ """
+ ),
+ NamedQuery(
+ name = "Project.allocateTopology",
+ query = """
+ UPDATE Project p
+ SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.topologiesCreated = :oldState
+ """
+ ),
+ NamedQuery(
+ name = "Project.allocateScenario",
+ query = """
+ UPDATE Project p
+ SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now
+ WHERE p.id = :id AND p.scenariosCreated = :oldState
+ """
+ )
+ ]
+)
+class Project(
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ val id: Long,
+
+ @Column(nullable = false)
+ var name: String,
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ val createdAt: Instant,
+) {
+ /**
+ * The instant at which the project was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ var updatedAt: Instant = createdAt
+
+ /**
+ * The portfolios belonging to this project.
+ */
+ @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
+ @OrderBy("id ASC")
+ val portfolios: MutableSet<Portfolio> = mutableSetOf()
+
+ /**
+ * The number of portfolios created for this project (including deleted portfolios).
+ */
+ @Column(name = "portfolios_created", nullable = false)
+ var portfoliosCreated: Int = 0
+
+ /**
+ * The topologies belonging to this project.
+ */
+ @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
+ @OrderBy("id ASC")
+ val topologies: MutableSet<Topology> = mutableSetOf()
+
+ /**
+ * The number of topologies created for this project (including deleted topologies).
+ */
+ @Column(name = "topologies_created", nullable = false)
+ var topologiesCreated: Int = 0
+
+ /**
+ * The scenarios belonging to this project.
+ */
+ @OneToMany(mappedBy = "project", orphanRemoval = true)
+ val scenarios: MutableSet<Scenario> = mutableSetOf()
+
+ /**
+ * The number of scenarios created for this project (including deleted scenarios).
+ */
+ @Column(name = "scenarios_created", nullable = false)
+ var scenariosCreated: Int = 0
+
+ /**
+ * The users authorized to access the project.
+ */
+ @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true)
+ val authorizations: MutableSet<ProjectAuthorization> = mutableSetOf()
+
+ /**
+ * Return a string representation of this project.
+ */
+ override fun toString(): String = "Project[id=$id,name=$name]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt
new file mode 100644
index 00000000..a72ff06a
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import org.opendc.web.proto.user.ProjectRole
+import javax.persistence.*
+
+/**
+ * An authorization for some user to participate in a project.
+ */
+@Entity
+@Table(name = "project_authorizations")
+class ProjectAuthorization(
+ /**
+ * The user identifier of the authorization.
+ */
+ @EmbeddedId
+ val key: ProjectAuthorizationKey,
+
+ /**
+ * The project that the user is authorized to participate in.
+ */
+ @ManyToOne(optional = false)
+ @MapsId("projectId")
+ @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false)
+ val project: Project,
+
+ /**
+ * The role of the user in the project.
+ */
+ @Column(nullable = false)
+ val role: ProjectRole
+) {
+ /**
+ * Return a string representation of this project authorization.
+ */
+ override fun toString(): String = "ProjectAuthorization[project=${key.projectId},user=${key.userId},role=$role]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt
new file mode 100644
index 00000000..b5f66e70
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import javax.persistence.Column
+import javax.persistence.Embeddable
+
+/**
+ * Key for representing a [ProjectAuthorization] object.
+ */
+@Embeddable
+data class ProjectAuthorizationKey(
+ @Column(name = "user_id", nullable = false)
+ val userId: String,
+
+ @Column(name = "project_id", nullable = false)
+ val projectId: Long
+) : java.io.Serializable
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt
new file mode 100644
index 00000000..5c9cb259
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
+import org.opendc.web.proto.OperationalPhenomena
+import javax.persistence.*
+
+/**
+ * A single scenario to be explored by the simulator.
+ */
+@TypeDef(name = "json", typeClass = JsonType::class)
+@Entity
+@Table(
+ name = "scenarios",
+ uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
+ indexes = [Index(name = "fn_scenarios_number", columnList = "project_id, number")]
+)
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Scenario.findAll",
+ query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"
+ ),
+ NamedQuery(
+ name = "Scenario.findAllForPortfolio",
+ query = """
+ SELECT s
+ FROM Scenario s
+ JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number
+ WHERE s.project.id = :projectId
+ """
+ ),
+ NamedQuery(
+ name = "Scenario.findOne",
+ query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number"
+ )
+ ]
+)
+class Scenario(
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ val id: Long,
+
+ /**
+ * Unique number of the scenario for the project.
+ */
+ @Column(nullable = false)
+ val number: Int,
+
+ @Column(nullable = false, updatable = false)
+ val name: String,
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "project_id", nullable = false)
+ val project: Project,
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "portfolio_id", nullable = false)
+ val portfolio: Portfolio,
+
+ @Embedded
+ val workload: Workload,
+
+ @ManyToOne(optional = false)
+ val topology: Topology,
+
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
+ val phenomena: OperationalPhenomena,
+
+ @Column(name = "scheduler_name", nullable = false, updatable = false)
+ val schedulerName: String,
+) {
+ /**
+ * The [Job] associated with the scenario.
+ */
+ @OneToOne(cascade = [CascadeType.ALL])
+ lateinit var job: Job
+
+ /**
+ * Return a string representation of this scenario.
+ */
+ override fun toString(): String = "Scenario[id=$id,name=$name,project=${project.id},portfolio=${portfolio.id}]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt
new file mode 100644
index 00000000..9b64e382
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
+import org.opendc.web.proto.Room
+import java.time.Instant
+import javax.persistence.*
+
+/**
+ * A datacenter design in OpenDC.
+ */
+@TypeDef(name = "json", typeClass = JsonType::class)
+@Entity
+@Table(
+ name = "topologies",
+ uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])],
+ indexes = [Index(name = "fn_topologies_number", columnList = "project_id, number")]
+)
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Topology.findAll",
+ query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"
+ ),
+ NamedQuery(
+ name = "Topology.findOne",
+ query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number"
+ )
+ ]
+)
+class Topology(
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ val id: Long,
+
+ /**
+ * Unique number of the topology for the project.
+ */
+ @Column(nullable = false)
+ val number: Int,
+
+ @Column(nullable = false)
+ val name: String,
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "project_id", nullable = false)
+ val project: Project,
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ val createdAt: Instant,
+
+ /**
+ * Datacenter design in JSON
+ */
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb", nullable = false)
+ var rooms: List<Room> = emptyList()
+) {
+ /**
+ * The instant at which the topology was updated.
+ */
+ @Column(name = "updated_at", nullable = false)
+ var updatedAt: Instant = createdAt
+
+ /**
+ * Return a string representation of this topology.
+ */
+ override fun toString(): String = "Topology[id=$id,name=$name,project=${project.id}]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt
new file mode 100644
index 00000000..2e2d71f8
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+package org.opendc.web.api.model
+
+import javax.persistence.*
+
+/**
+ * A workload trace available for simulation.
+ *
+ * @param id The unique identifier of the trace.
+ * @param name The name of the trace.
+ * @param type The type of trace.
+ */
+@Entity
+@Table(name = "traces")
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "Trace.findAll",
+ query = "SELECT t FROM Trace t"
+ ),
+ ]
+)
+class Trace(
+ @Id
+ val id: String,
+
+ @Column(nullable = false, updatable = false)
+ val name: String,
+
+ @Column(nullable = false, updatable = false)
+ val type: String,
+) {
+ /**
+ * Return a string representation of this trace.
+ */
+ override fun toString(): String = "Trace[id=$id,name=$name,type=$type]"
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt
new file mode 100644
index 00000000..07fc096b
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.model
+
+import javax.persistence.Column
+import javax.persistence.Embeddable
+import javax.persistence.ManyToOne
+
+/**
+ * Specification of the workload for a [Scenario].
+ */
+@Embeddable
+class Workload(
+ @ManyToOne(optional = false)
+ val trace: Trace,
+
+ @Column(name = "sampling_fraction", nullable = false, updatable = false)
+ val samplingFraction: Double
+)
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt
new file mode 100644
index 00000000..558d7c38
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Job
+import org.opendc.web.proto.JobState
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Job] entities.
+ */
+@ApplicationScoped
+class JobRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find all jobs currently residing in [state].
+ *
+ * @param state The state in which the jobs should be.
+ * @return The list of jobs in state [state].
+ */
+ fun findAll(state: JobState): List<Job> {
+ return em.createNamedQuery("Job.findAll", Job::class.java)
+ .setParameter("state", state)
+ .resultList
+ }
+
+ /**
+ * Find the [Job] with the specified [id].
+ *
+ * @param id The unique identifier of the job.
+ * @return The trace or `null` if it does not exist.
+ */
+ fun findOne(id: Long): Job? {
+ return em.find(Job::class.java, id)
+ }
+
+ /**
+ * Delete the specified [job].
+ */
+ fun delete(job: Job) {
+ em.remove(job)
+ }
+
+ /**
+ * Save the specified [job] to the database.
+ */
+ fun save(job: Job) {
+ em.persist(job)
+ }
+
+ /**
+ * Atomically update the specified [job].
+ *
+ * @param job The job to update atomically.
+ * @param newState The new state to enter into.
+ * @param time The time at which the update occurs.
+ * @param results The results to possible set.
+ * @return `true` when the update succeeded`, `false` when there was a conflict.
+ */
+ fun updateOne(job: Job, newState: JobState, time: Instant, results: Map<String, Any>?): Boolean {
+ val count = em.createNamedQuery("Job.updateOne")
+ .setParameter("id", job.id)
+ .setParameter("oldState", job.state)
+ .setParameter("newState", newState)
+ .setParameter("updatedAt", Instant.now())
+ .setParameter("results", results)
+ .executeUpdate()
+ em.refresh(job)
+ return count > 0
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt
new file mode 100644
index 00000000..34b3598c
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Portfolio
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Portfolio] entities.
+ */
+@ApplicationScoped
+class PortfolioRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find all [Portfolio]s that belong to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The list of portfolios that belong to the specified project.
+ */
+ fun findAll(projectId: Long): List<Portfolio> {
+ return em.createNamedQuery("Portfolio.findAll", Portfolio::class.java)
+ .setParameter("projectId", projectId)
+ .resultList
+ }
+
+ /**
+ * Find the [Portfolio] with the specified [number] belonging to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the portfolio.
+ * @return The portfolio or `null` if it does not exist.
+ */
+ fun findOne(projectId: Long, number: Int): Portfolio? {
+ return em.createNamedQuery("Portfolio.findOne", Portfolio::class.java)
+ .setParameter("projectId", projectId)
+ .setParameter("number", number)
+ .setMaxResults(1)
+ .resultList
+ .firstOrNull()
+ }
+
+ /**
+ * Delete the specified [portfolio].
+ */
+ fun delete(portfolio: Portfolio) {
+ em.remove(portfolio)
+ }
+
+ /**
+ * Save the specified [portfolio] to the database.
+ */
+ fun save(portfolio: Portfolio) {
+ em.persist(portfolio)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt
new file mode 100644
index 00000000..6529f778
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Project
+import org.opendc.web.api.model.ProjectAuthorization
+import org.opendc.web.api.model.ProjectAuthorizationKey
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Project] entities.
+ */
+@ApplicationScoped
+class ProjectRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * List all projects for the user with the specified [userId].
+ *
+ * @param userId The identifier of the user that is requesting the list of projects.
+ * @return A list of projects that the user has received authorization for.
+ */
+ fun findAll(userId: String): List<ProjectAuthorization> {
+ return em.createNamedQuery("Project.findAll", ProjectAuthorization::class.java)
+ .setParameter("userId", userId)
+ .resultList
+ }
+
+ /**
+ * Find the project with [id] for the user with the specified [userId].
+ *
+ * @param userId The identifier of the user that is requesting the list of projects.
+ * @param id The unique identifier of the project.
+ * @return The project with the specified identifier or `null` if it does not exist or is not accessible to the
+ * user with the specified identifier.
+ */
+ fun findOne(userId: String, id: Long): ProjectAuthorization? {
+ return em.find(ProjectAuthorization::class.java, ProjectAuthorizationKey(userId, id))
+ }
+
+ /**
+ * Delete the specified [project].
+ */
+ fun delete(project: Project) {
+ em.remove(project)
+ }
+
+ /**
+ * Save the specified [project] to the database.
+ */
+ fun save(project: Project) {
+ em.persist(project)
+ }
+
+ /**
+ * Save the specified [auth] to the database.
+ */
+ fun save(auth: ProjectAuthorization) {
+ em.persist(auth)
+ }
+
+ /**
+ * Allocate the next portfolio number for the specified [project].
+ *
+ * @param project The project to allocate the portfolio number for.
+ * @param time The time at which the new portfolio is created.
+ * @param tries The number of times to try to allocate the number before failing.
+ */
+ fun allocatePortfolio(project: Project, time: Instant, tries: Int = 4): Int {
+ repeat(tries) {
+ val count = em.createNamedQuery("Project.allocatePortfolio")
+ .setParameter("id", project.id)
+ .setParameter("oldState", project.portfoliosCreated)
+ .setParameter("now", time)
+ .executeUpdate()
+
+ if (count > 0) {
+ return project.portfoliosCreated + 1
+ } else {
+ em.refresh(project)
+ }
+ }
+
+ throw IllegalStateException("Failed to allocate next portfolio")
+ }
+
+ /**
+ * Allocate the next topology number for the specified [project].
+ *
+ * @param project The project to allocate the topology number for.
+ * @param time The time at which the new topology is created.
+ * @param tries The number of times to try to allocate the number before failing.
+ */
+ fun allocateTopology(project: Project, time: Instant, tries: Int = 4): Int {
+ repeat(tries) {
+ val count = em.createNamedQuery("Project.allocateTopology")
+ .setParameter("id", project.id)
+ .setParameter("oldState", project.topologiesCreated)
+ .setParameter("now", time)
+ .executeUpdate()
+
+ if (count > 0) {
+ return project.topologiesCreated + 1
+ } else {
+ em.refresh(project)
+ }
+ }
+
+ throw IllegalStateException("Failed to allocate next topology")
+ }
+
+ /**
+ * Allocate the next scenario number for the specified [project].
+ *
+ * @param project The project to allocate the scenario number for.
+ * @param time The time at which the new scenario is created.
+ * @param tries The number of times to try to allocate the number before failing.
+ */
+ fun allocateScenario(project: Project, time: Instant, tries: Int = 4): Int {
+ repeat(tries) {
+ val count = em.createNamedQuery("Project.allocateScenario")
+ .setParameter("id", project.id)
+ .setParameter("oldState", project.scenariosCreated)
+ .setParameter("now", time)
+ .executeUpdate()
+
+ if (count > 0) {
+ return project.scenariosCreated + 1
+ } else {
+ em.refresh(project)
+ }
+ }
+
+ throw IllegalStateException("Failed to allocate next scenario")
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt
new file mode 100644
index 00000000..de116ad6
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Scenario
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Scenario] entities.
+ */
+@ApplicationScoped
+class ScenarioRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find all [Scenario]s that belong to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The list of scenarios that belong to the specified project.
+ */
+ fun findAll(projectId: Long): List<Scenario> {
+ return em.createNamedQuery("Scenario.findAll", Scenario::class.java)
+ .setParameter("projectId", projectId)
+ .resultList
+ }
+
+ /**
+ * Find all [Scenario]s that belong to [portfolio][number] of [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the portfolio to which the scenarios should belong.
+ * @return The list of scenarios that belong to the specified portfolio.
+ */
+ fun findAll(projectId: Long, number: Int): List<Scenario> {
+ return em.createNamedQuery("Scenario.findAllForPortfolio", Scenario::class.java)
+ .setParameter("projectId", projectId)
+ .setParameter("number", number)
+ .resultList
+ }
+
+ /**
+ * Find the [Scenario] with the specified [number] belonging to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the scenario.
+ * @return The scenario or `null` if it does not exist.
+ */
+ fun findOne(projectId: Long, number: Int): Scenario? {
+ return em.createNamedQuery("Scenario.findOne", Scenario::class.java)
+ .setParameter("projectId", projectId)
+ .setParameter("number", number)
+ .setMaxResults(1)
+ .resultList
+ .firstOrNull()
+ }
+
+ /**
+ * Delete the specified [scenario].
+ */
+ fun delete(scenario: Scenario) {
+ em.remove(scenario)
+ }
+
+ /**
+ * Save the specified [scenario] to the database.
+ */
+ fun save(scenario: Scenario) {
+ em.persist(scenario)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt
new file mode 100644
index 00000000..cd8f666e
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Topology
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Topology] entities.
+ */
+@ApplicationScoped
+class TopologyRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find all [Topology]s that belong to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @return The list of topologies that belong to the specified project.
+ */
+ fun findAll(projectId: Long): List<Topology> {
+ return em.createNamedQuery("Topology.findAll", Topology::class.java)
+ .setParameter("projectId", projectId)
+ .resultList
+ }
+
+ /**
+ * Find the [Topology] with the specified [number] belonging to [project][projectId].
+ *
+ * @param projectId The unique identifier of the project.
+ * @param number The number of the topology.
+ * @return The topology or `null` if it does not exist.
+ */
+ fun findOne(projectId: Long, number: Int): Topology? {
+ return em.createNamedQuery("Topology.findOne", Topology::class.java)
+ .setParameter("projectId", projectId)
+ .setParameter("number", number)
+ .setMaxResults(1)
+ .resultList
+ .firstOrNull()
+ }
+
+ /**
+ * Find the [Topology] with the specified [id].
+ *
+ * @param id Unique identifier of the topology.
+ * @return The topology or `null` if it does not exist.
+ */
+ fun findOne(id: Long): Topology? {
+ return em.find(Topology::class.java, id)
+ }
+
+ /**
+ * Delete the specified [topology].
+ */
+ fun delete(topology: Topology) {
+ em.remove(topology)
+ }
+
+ /**
+ * Save the specified [topology] to the database.
+ */
+ fun save(topology: Topology) {
+ em.persist(topology)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt
new file mode 100644
index 00000000..6652fc80
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.repository
+
+import org.opendc.web.api.model.Trace
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [Trace] entities.
+ */
+@ApplicationScoped
+class TraceRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find all workload traces in the database.
+ *
+ * @return The list of available workload traces.
+ */
+ fun findAll(): List<Trace> {
+ return em.createNamedQuery("Trace.findAll", Trace::class.java).resultList
+ }
+
+ /**
+ * Find the [Trace] with the specified [id].
+ *
+ * @param id The unique identifier of the trace.
+ * @return The trace or `null` if it does not exist.
+ */
+ fun findOne(id: String): Trace? {
+ return em.find(Trace::class.java, id)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt
new file mode 100644
index 00000000..735fdd9b
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+package org.opendc.web.api.rest
+
+import javax.ws.rs.GET
+import javax.ws.rs.Path
+
+/**
+ * A resource representing the available schedulers that can be used during experiments.
+ */
+@Path("/schedulers")
+class SchedulerResource {
+ /**
+ * Obtain all available schedulers.
+ */
+ @GET
+ fun getAll() = listOf(
+ "mem",
+ "mem-inv",
+ "core-mem",
+ "core-mem-inv",
+ "active-servers",
+ "active-servers-inv",
+ "provisioned-cores",
+ "provisioned-cores-inv",
+ "random"
+ )
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt
new file mode 100644
index 00000000..e87fe602
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+package org.opendc.web.api.rest
+
+import org.opendc.web.api.service.TraceService
+import org.opendc.web.proto.Trace
+import javax.inject.Inject
+import javax.ws.rs.*
+
+/**
+ * A resource representing the workload traces available in the OpenDC instance.
+ */
+@Path("/traces")
+class TraceResource @Inject constructor(private val traceService: TraceService) {
+ /**
+ * Obtain all available traces.
+ */
+ @GET
+ fun getAll(): List<Trace> {
+ return traceService.findAll()
+ }
+
+ /**
+ * Obtain trace information by identifier.
+ */
+ @GET
+ @Path("{id}")
+ fun get(@PathParam("id") id: String): Trace {
+ return traceService.findById(id) ?: throw WebApplicationException("Trace not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt
new file mode 100644
index 00000000..fb253758
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.error
+
+import org.opendc.web.proto.ProtocolError
+import javax.ws.rs.WebApplicationException
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.ext.ExceptionMapper
+import javax.ws.rs.ext.Provider
+
+/**
+ * Helper class to transform an exception into an JSON error response.
+ */
+@Provider
+class GenericExceptionMapper : ExceptionMapper<Exception> {
+ override fun toResponse(exception: Exception): Response {
+ val code = if (exception is WebApplicationException) exception.response.status else 500
+
+ return Response.status(code)
+ .entity(ProtocolError(code, exception.message ?: "Unknown error"))
+ .type(MediaType.APPLICATION_JSON)
+ .build()
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt
new file mode 100644
index 00000000..57cd35d1
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.error
+
+import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
+import org.opendc.web.proto.ProtocolError
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.ext.ExceptionMapper
+import javax.ws.rs.ext.Provider
+
+/**
+ * An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson.
+ */
+@Provider
+class MissingKotlinParameterExceptionMapper : ExceptionMapper<MissingKotlinParameterException> {
+ override fun toResponse(exception: MissingKotlinParameterException): Response {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity(ProtocolError(Response.Status.BAD_REQUEST.statusCode, "Field '${exception.parameter.name}' is missing from body."))
+ .type(MediaType.APPLICATION_JSON)
+ .build()
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt
new file mode 100644
index 00000000..7e31e2c5
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.runner
+
+import org.opendc.web.api.service.JobService
+import org.opendc.web.proto.runner.Job
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.validation.Valid
+import javax.ws.rs.*
+
+/**
+ * A resource representing the available simulation jobs.
+ */
+@Path("/jobs")
+@RolesAllowed("runner")
+class JobResource @Inject constructor(private val jobService: JobService) {
+ /**
+ * Obtain all pending simulation jobs.
+ */
+ @GET
+ fun queryPending(): List<Job> {
+ return jobService.queryPending()
+ }
+
+ /**
+ * Get a job by identifier.
+ */
+ @GET
+ @Path("{job}")
+ fun get(@PathParam("job") id: Long): Job {
+ return jobService.findById(id) ?: throw WebApplicationException("Job not found", 404)
+ }
+
+ /**
+ * Atomically update the state of a job.
+ */
+ @POST
+ @Path("{job}")
+ @Transactional
+ fun update(@PathParam("job") id: Long, @Valid update: Job.Update): Job {
+ return jobService.updateState(id, update.state, update.results) ?: throw WebApplicationException("Job not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt
new file mode 100644
index 00000000..e720de75
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.api.service.PortfolioService
+import org.opendc.web.proto.user.Portfolio
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.validation.Valid
+import javax.ws.rs.*
+
+/**
+ * A resource representing the portfolios of a project.
+ */
+@Path("/projects/{project}/portfolios")
+@RolesAllowed("openid")
+class PortfolioResource @Inject constructor(
+ private val portfolioService: PortfolioService,
+ private val identity: SecurityIdentity,
+) {
+ /**
+ * Get all portfolios that belong to the specified project.
+ */
+ @GET
+ fun getAll(@PathParam("project") projectId: Long): List<Portfolio> {
+ return portfolioService.findAll(identity.principal.name, projectId)
+ }
+
+ /**
+ * Create a portfolio for this project.
+ */
+ @POST
+ @Transactional
+ fun create(@PathParam("project") projectId: Long, @Valid request: Portfolio.Create): Portfolio {
+ return portfolioService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Project not found", 404)
+ }
+
+ /**
+ * Obtain a portfolio by its identifier.
+ */
+ @GET
+ @Path("{portfolio}")
+ fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio {
+ return portfolioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404)
+ }
+
+ /**
+ * Delete a portfolio.
+ */
+ @DELETE
+ @Path("{portfolio}")
+ fun delete(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio {
+ return portfolioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt
new file mode 100644
index 00000000..8d24b2eb
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.api.service.ScenarioService
+import org.opendc.web.proto.user.Scenario
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.validation.Valid
+import javax.ws.rs.*
+
+/**
+ * A resource representing the scenarios of a portfolio.
+ */
+@Path("/projects/{project}/portfolios/{portfolio}/scenarios")
+@RolesAllowed("openid")
+class PortfolioScenarioResource @Inject constructor(
+ private val scenarioService: ScenarioService,
+ private val identity: SecurityIdentity,
+) {
+ /**
+ * Get all scenarios that belong to the specified portfolio.
+ */
+ @GET
+ fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int): List<Scenario> {
+ return scenarioService.findAll(identity.principal.name, projectId, portfolioNumber)
+ }
+
+ /**
+ * Create a scenario for this portfolio.
+ */
+ @POST
+ @Transactional
+ fun create(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int, @Valid request: Scenario.Create): Scenario {
+ return scenarioService.create(identity.principal.name, projectId, portfolioNumber, request) ?: throw WebApplicationException("Portfolio not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt
new file mode 100644
index 00000000..a27d50e7
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.api.service.ProjectService
+import org.opendc.web.proto.user.Project
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.validation.Valid
+import javax.ws.rs.*
+
+/**
+ * A resource representing the created projects.
+ */
+@Path("/projects")
+@RolesAllowed("openid")
+class ProjectResource @Inject constructor(
+ private val projectService: ProjectService,
+ private val identity: SecurityIdentity
+) {
+ /**
+ * Obtain all the projects of the current user.
+ */
+ @GET
+ fun getAll(): List<Project> {
+ return projectService.findWithUser(identity.principal.name)
+ }
+
+ /**
+ * Create a new project for the current user.
+ */
+ @POST
+ @Transactional
+ fun create(@Valid request: Project.Create): Project {
+ return projectService.createForUser(identity.principal.name, request.name)
+ }
+
+ /**
+ * Obtain a single project by its identifier.
+ */
+ @GET
+ @Path("{project}")
+ fun get(@PathParam("project") id: Long): Project {
+ return projectService.findWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404)
+ }
+
+ /**
+ * Delete a project.
+ */
+ @DELETE
+ @Path("{project}")
+ @Transactional
+ fun delete(@PathParam("project") id: Long): Project {
+ try {
+ return projectService.deleteWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404)
+ } catch (e: IllegalArgumentException) {
+ throw WebApplicationException(e.message, 403)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt
new file mode 100644
index 00000000..3690f987
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.api.service.ScenarioService
+import org.opendc.web.proto.user.Scenario
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.ws.rs.*
+
+/**
+ * A resource representing the scenarios of a portfolio.
+ */
+@Path("/projects/{project}/scenarios")
+@RolesAllowed("openid")
+class ScenarioResource @Inject constructor(
+ private val scenarioService: ScenarioService,
+ private val identity: SecurityIdentity
+) {
+ /**
+ * Obtain a scenario by its identifier.
+ */
+ @GET
+ @Path("{scenario}")
+ fun get(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario {
+ return scenarioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404)
+ }
+
+ /**
+ * Delete a scenario.
+ */
+ @DELETE
+ @Path("{scenario}")
+ @Transactional
+ fun delete(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario {
+ return scenarioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt
new file mode 100644
index 00000000..52c5eaaa
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.api.service.TopologyService
+import org.opendc.web.proto.user.Topology
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.transaction.Transactional
+import javax.validation.Valid
+import javax.ws.rs.*
+
+/**
+ * A resource representing the constructed datacenter topologies.
+ */
+@Path("/projects/{project}/topologies")
+@RolesAllowed("openid")
+class TopologyResource @Inject constructor(
+ private val topologyService: TopologyService,
+ private val identity: SecurityIdentity
+) {
+ /**
+ * Get all topologies that belong to the specified project.
+ */
+ @GET
+ fun getAll(@PathParam("project") projectId: Long): List<Topology> {
+ return topologyService.findAll(identity.principal.name, projectId)
+ }
+
+ /**
+ * Create a topology for this project.
+ */
+ @POST
+ @Transactional
+ fun create(@PathParam("project") projectId: Long, @Valid request: Topology.Create): Topology {
+ return topologyService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Topology not found", 404)
+ }
+
+ /**
+ * Obtain a topology by its number.
+ */
+ @GET
+ @Path("{topology}")
+ fun get(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology {
+ return topologyService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404)
+ }
+
+ /**
+ * Update the specified topology by its number.
+ */
+ @PUT
+ @Path("{topology}")
+ @Transactional
+ fun update(@PathParam("project") projectId: Long, @PathParam("topology") number: Int, @Valid request: Topology.Update): Topology {
+ return topologyService.update(identity.principal.name, projectId, number, request) ?: throw WebApplicationException("Topology not found", 404)
+ }
+
+ /**
+ * Delete the specified topology.
+ */
+ @Path("{topology}")
+ @DELETE
+ @Transactional
+ fun delete(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology {
+ return topologyService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt
new file mode 100644
index 00000000..1b33248d
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.repository.JobRepository
+import org.opendc.web.proto.JobState
+import org.opendc.web.proto.runner.Job
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+/**
+ * Service for managing [Job]s.
+ */
+@ApplicationScoped
+class JobService @Inject constructor(private val repository: JobRepository) {
+ /**
+ * Query the pending simulation jobs.
+ */
+ fun queryPending(): List<Job> {
+ return repository.findAll(JobState.PENDING).map { it.toRunnerDto() }
+ }
+
+ /**
+ * Find a job by its identifier.
+ */
+ fun findById(id: Long): Job? {
+ return repository.findOne(id)?.toRunnerDto()
+ }
+
+ /**
+ * Atomically update the state of a [Job].
+ */
+ fun updateState(id: Long, newState: JobState, results: Map<String, Any>?): Job? {
+ val entity = repository.findOne(id) ?: return null
+ val state = entity.state
+ if (!state.isTransitionLegal(newState)) {
+ throw IllegalArgumentException("Invalid transition from $state to $newState")
+ }
+
+ val now = Instant.now()
+ if (!repository.updateOne(entity, newState, now, results)) {
+ throw IllegalStateException("Conflicting update")
+ }
+
+ return entity.toRunnerDto()
+ }
+
+ /**
+ * Determine whether the transition from [this] to [newState] is legal.
+ */
+ private fun JobState.isTransitionLegal(newState: JobState): Boolean {
+ // Note that we always allow transitions from the state
+ return newState == this || when (this) {
+ JobState.PENDING -> newState == JobState.CLAIMED
+ JobState.CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED
+ JobState.RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED
+ JobState.FINISHED, JobState.FAILED -> false
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt
new file mode 100644
index 00000000..1f41c2d7
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.model.*
+import org.opendc.web.api.repository.PortfolioRepository
+import org.opendc.web.api.repository.ProjectRepository
+import org.opendc.web.proto.user.Portfolio
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import org.opendc.web.api.model.Portfolio as PortfolioEntity
+
+/**
+ * Service for managing [Portfolio]s.
+ */
+@ApplicationScoped
+class PortfolioService @Inject constructor(
+ private val projectRepository: ProjectRepository,
+ private val portfolioRepository: PortfolioRepository
+) {
+ /**
+ * List all [Portfolio]s that belong a certain project.
+ */
+ fun findAll(userId: String, projectId: Long): List<Portfolio> {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
+ val project = auth.toUserDto()
+ return portfolioRepository.findAll(projectId).map { it.toUserDto(project) }
+ }
+
+ /**
+ * Find a [Portfolio] with the specified [number] belonging to [project][projectId].
+ */
+ fun findOne(userId: String, projectId: Long, number: Int): Portfolio? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return null
+ return portfolioRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto())
+ }
+
+ /**
+ * Delete the portfolio with the specified [number] belonging to [project][projectId].
+ */
+ fun delete(userId: String, projectId: Long, number: Int): Portfolio? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val entity = portfolioRepository.findOne(projectId, number) ?: return null
+ val portfolio = entity.toUserDto(auth.toUserDto())
+ portfolioRepository.delete(entity)
+ return portfolio
+ }
+
+ /**
+ * Construct a new [Portfolio] with the specified name.
+ */
+ fun create(userId: String, projectId: Long, request: Portfolio.Create): Portfolio? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val now = Instant.now()
+ val project = auth.project
+ val number = projectRepository.allocatePortfolio(auth.project, now)
+
+ val portfolio = PortfolioEntity(0, number, request.name, project, request.targets)
+
+ project.portfolios.add(portfolio)
+ portfolioRepository.save(portfolio)
+
+ return portfolio.toUserDto(auth.toUserDto())
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt
new file mode 100644
index 00000000..c3e43395
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.model.*
+import org.opendc.web.api.repository.ProjectRepository
+import org.opendc.web.proto.user.Project
+import org.opendc.web.proto.user.ProjectRole
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+/**
+ * Service for managing [Project]s.
+ */
+@ApplicationScoped
+class ProjectService @Inject constructor(private val repository: ProjectRepository) {
+ /**
+ * List all projects for the user with the specified [userId].
+ */
+ fun findWithUser(userId: String): List<Project> {
+ return repository.findAll(userId).map { it.toUserDto() }
+ }
+
+ /**
+ * Obtain the project with the specified [id] for the user with the specified [userId].
+ */
+ fun findWithUser(userId: String, id: Long): Project? {
+ return repository.findOne(userId, id)?.toUserDto()
+ }
+
+ /**
+ * Create a new [Project] for the user with the specified [userId].
+ */
+ fun createForUser(userId: String, name: String): Project {
+ val now = Instant.now()
+ val entity = Project(0, name, now)
+ repository.save(entity)
+
+ val authorization = ProjectAuthorization(ProjectAuthorizationKey(userId, entity.id), entity, ProjectRole.OWNER)
+
+ entity.authorizations.add(authorization)
+ repository.save(authorization)
+
+ return authorization.toUserDto()
+ }
+
+ /**
+ * Delete a project by its identifier.
+ *
+ * @param userId The user that invokes the action.
+ * @param id The identifier of the project.
+ */
+ fun deleteWithUser(userId: String, id: Long): Project? {
+ val auth = repository.findOne(userId, id) ?: return null
+
+ if (!auth.role.canDelete) {
+ throw IllegalArgumentException("Not allowed to delete project")
+ }
+
+ val now = Instant.now()
+ val project = auth.toUserDto().copy(updatedAt = now)
+ repository.delete(auth.project)
+ return project
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt
new file mode 100644
index 00000000..3722a641
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.model.Job
+import org.opendc.web.api.model.Portfolio
+import org.opendc.web.api.model.Scenario
+import org.opendc.web.api.model.Topology
+
+/**
+ * Conversions into DTOs provided to OpenDC runners.
+ */
+
+/**
+ * Convert a [Topology] into a runner-facing DTO.
+ */
+internal fun Topology.toRunnerDto(): org.opendc.web.proto.runner.Topology {
+ return org.opendc.web.proto.runner.Topology(id, number, name, rooms, createdAt, updatedAt)
+}
+
+/**
+ * Convert a [Portfolio] into a runner-facing DTO.
+ */
+internal fun Portfolio.toRunnerDto(): org.opendc.web.proto.runner.Portfolio {
+ return org.opendc.web.proto.runner.Portfolio(id, number, name, targets)
+}
+
+/**
+ * Convert a [Job] into a runner-facing DTO.
+ */
+internal fun Job.toRunnerDto(): org.opendc.web.proto.runner.Job {
+ return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, results)
+}
+
+/**
+ * Convert a [Job] into a runner-facing DTO.
+ */
+internal fun Scenario.toRunnerDto(): org.opendc.web.proto.runner.Scenario {
+ return org.opendc.web.proto.runner.Scenario(
+ id,
+ number,
+ portfolio.toRunnerDto(),
+ name,
+ workload.toDto(),
+ topology.toRunnerDto(),
+ phenomena,
+ schedulerName
+ )
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt
new file mode 100644
index 00000000..dd51a929
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.model.*
+import org.opendc.web.api.repository.*
+import org.opendc.web.proto.user.Scenario
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+/**
+ * Service for managing [Scenario]s.
+ */
+@ApplicationScoped
+class ScenarioService @Inject constructor(
+ private val projectRepository: ProjectRepository,
+ private val portfolioRepository: PortfolioRepository,
+ private val topologyRepository: TopologyRepository,
+ private val traceRepository: TraceRepository,
+ private val scenarioRepository: ScenarioRepository,
+) {
+ /**
+ * List all [Scenario]s that belong a certain portfolio.
+ */
+ fun findAll(userId: String, projectId: Long, number: Int): List<Scenario> {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
+ val project = auth.toUserDto()
+ return scenarioRepository.findAll(projectId).map { it.toUserDto(project) }
+ }
+
+ /**
+ * Obtain a [Scenario] by identifier.
+ */
+ fun findOne(userId: String, projectId: Long, number: Int): Scenario? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return null
+ val project = auth.toUserDto()
+ return scenarioRepository.findOne(projectId, number)?.toUserDto(project)
+ }
+
+ /**
+ * Delete the specified scenario.
+ */
+ fun delete(userId: String, projectId: Long, number: Int): Scenario? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val entity = scenarioRepository.findOne(projectId, number) ?: return null
+ val scenario = entity.toUserDto(auth.toUserDto())
+ scenarioRepository.delete(entity)
+ return scenario
+ }
+
+ /**
+ * Construct a new [Scenario] with the specified data.
+ */
+ fun create(userId: String, projectId: Long, portfolioNumber: Int, request: Scenario.Create): Scenario? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val portfolio = portfolioRepository.findOne(projectId, portfolioNumber) ?: return null
+ val topology = requireNotNull(
+ topologyRepository.findOne(
+ projectId,
+ request.topology.toInt()
+ )
+ ) { "Referred topology does not exist" }
+ val trace =
+ requireNotNull(traceRepository.findOne(request.workload.trace)) { "Referred trace does not exist" }
+
+ val now = Instant.now()
+ val project = auth.project
+ val number = projectRepository.allocateScenario(auth.project, now)
+
+ val scenario = Scenario(
+ 0,
+ number,
+ request.name,
+ project,
+ portfolio,
+ Workload(trace, request.workload.samplingFraction),
+ topology,
+ request.phenomena,
+ request.schedulerName
+ )
+ val job = Job(0, scenario, now, portfolio.targets.repeats)
+
+ scenario.job = job
+ portfolio.scenarios.add(scenario)
+ scenarioRepository.save(scenario)
+
+ return scenario.toUserDto(auth.toUserDto())
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt
new file mode 100644
index 00000000..f3460496
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.repository.ProjectRepository
+import org.opendc.web.api.repository.TopologyRepository
+import org.opendc.web.proto.user.Topology
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import org.opendc.web.api.model.Topology as TopologyEntity
+
+/**
+ * Service for managing [Topology]s.
+ */
+@ApplicationScoped
+class TopologyService @Inject constructor(
+ private val projectRepository: ProjectRepository,
+ private val topologyRepository: TopologyRepository
+) {
+ /**
+ * List all [Topology]s that belong a certain project.
+ */
+ fun findAll(userId: String, projectId: Long): List<Topology> {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return emptyList()
+ val project = auth.toUserDto()
+ return topologyRepository.findAll(projectId).map { it.toUserDto(project) }
+ }
+
+ /**
+ * Find the [Topology] with the specified [number] belonging to [project][projectId].
+ */
+ fun findOne(userId: String, projectId: Long, number: Int): Topology? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId) ?: return null
+ return topologyRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto())
+ }
+
+ /**
+ * Delete the [Topology] with the specified [number] belonging to [project][projectId].
+ */
+ fun delete(userId: String, projectId: Long, number: Int): Topology? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val entity = topologyRepository.findOne(projectId, number) ?: return null
+ val now = Instant.now()
+ val topology = entity.toUserDto(auth.toUserDto()).copy(updatedAt = now)
+ topologyRepository.delete(entity)
+
+ return topology
+ }
+
+ /**
+ * Update a [Topology] with the specified [number] belonging to [project][projectId].
+ */
+ fun update(userId: String, projectId: Long, number: Int, request: Topology.Update): Topology? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val entity = topologyRepository.findOne(projectId, number) ?: return null
+ val now = Instant.now()
+
+ entity.updatedAt = now
+ entity.rooms = request.rooms
+
+ return entity.toUserDto(auth.toUserDto())
+ }
+
+ /**
+ * Construct a new [Topology] with the specified name.
+ */
+ fun create(userId: String, projectId: Long, request: Topology.Create): Topology? {
+ // User must have access to project
+ val auth = projectRepository.findOne(userId, projectId)
+
+ if (auth == null) {
+ return null
+ } else if (!auth.role.canEdit) {
+ throw IllegalStateException("Not permitted to edit project")
+ }
+
+ val now = Instant.now()
+ val project = auth.project
+ val number = projectRepository.allocateTopology(auth.project, now)
+
+ val topology = TopologyEntity(0, number, request.name, project, now, request.rooms)
+
+ project.topologies.add(topology)
+ topologyRepository.save(topology)
+
+ return topology.toUserDto(auth.toUserDto())
+ }
+}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiResult.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt
index a3df01c5..a942696e 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiResult.kt
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt
@@ -20,24 +20,29 @@
* SOFTWARE.
*/
-package org.opendc.web.client
+package org.opendc.web.api.service
-import com.fasterxml.jackson.annotation.JsonSubTypes
-import com.fasterxml.jackson.annotation.JsonTypeInfo
+import org.opendc.web.api.repository.TraceRepository
+import org.opendc.web.proto.Trace
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
/**
- * Generic response model for the OpenDC API.
+ * Service for managing [Trace]s.
*/
-@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
-@JsonSubTypes(JsonSubTypes.Type(ApiResult.Success::class), JsonSubTypes.Type(ApiResult.Failure::class))
-public sealed class ApiResult<out T> {
+@ApplicationScoped
+class TraceService @Inject constructor(private val repository: TraceRepository) {
/**
- * A response indicating everything is okay.
+ * Obtain all available workload traces.
*/
- public data class Success<out T>(val data: T) : ApiResult<T>()
+ fun findAll(): List<Trace> {
+ return repository.findAll().map { it.toUserDto() }
+ }
/**
- * A response indicating a failure.
+ * Obtain a workload trace by identifier.
*/
- public data class Failure<out T>(val message: String, val errors: List<String> = emptyList()) : ApiResult<T>()
+ fun findById(id: String): Trace? {
+ return repository.findOne(id)?.toUserDto()
+ }
}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt
new file mode 100644
index 00000000..8612ee8c
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.api.model.*
+import org.opendc.web.proto.user.Project
+
+/**
+ * Conversions into DTOs provided to users.
+ */
+
+/**
+ * Convert a [Trace] entity into a [org.opendc.web.proto.Trace] DTO.
+ */
+internal fun Trace.toUserDto(): org.opendc.web.proto.Trace {
+ return org.opendc.web.proto.Trace(id, name, type)
+}
+
+/**
+ * Convert a [ProjectAuthorization] entity into a [Project] DTO.
+ */
+internal fun ProjectAuthorization.toUserDto(): Project {
+ return Project(project.id, project.name, project.createdAt, project.updatedAt, role)
+}
+
+/**
+ * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology] DTO.
+ */
+internal fun Topology.toUserDto(project: Project): org.opendc.web.proto.user.Topology {
+ return org.opendc.web.proto.user.Topology(id, number, project, name, rooms, createdAt, updatedAt)
+}
+
+/**
+ * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology.Summary] DTO.
+ */
+private fun Topology.toSummaryDto(): org.opendc.web.proto.user.Topology.Summary {
+ return org.opendc.web.proto.user.Topology.Summary(id, number, name, createdAt, updatedAt)
+}
+
+/**
+ * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio] DTO.
+ */
+internal fun Portfolio.toUserDto(project: Project): org.opendc.web.proto.user.Portfolio {
+ return org.opendc.web.proto.user.Portfolio(id, number, project, name, targets, scenarios.map { it.toSummaryDto() })
+}
+
+/**
+ * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio.Summary] DTO.
+ */
+private fun Portfolio.toSummaryDto(): org.opendc.web.proto.user.Portfolio.Summary {
+ return org.opendc.web.proto.user.Portfolio.Summary(id, number, name, targets)
+}
+
+/**
+ * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario] DTO.
+ */
+internal fun Scenario.toUserDto(project: Project): org.opendc.web.proto.user.Scenario {
+ return org.opendc.web.proto.user.Scenario(
+ id,
+ number,
+ project,
+ portfolio.toSummaryDto(),
+ name,
+ workload.toDto(),
+ topology.toSummaryDto(),
+ phenomena,
+ schedulerName,
+ job.toUserDto()
+ )
+}
+
+/**
+ * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario.Summary] DTO.
+ */
+private fun Scenario.toSummaryDto(): org.opendc.web.proto.user.Scenario.Summary {
+ return org.opendc.web.proto.user.Scenario.Summary(
+ id,
+ number,
+ name,
+ workload.toDto(),
+ topology.toSummaryDto(),
+ phenomena,
+ schedulerName,
+ job.toUserDto()
+ )
+}
+
+/**
+ * Convert a [Job] entity into a [org.opendc.web.proto.user.Job] DTO.
+ */
+internal fun Job.toUserDto(): org.opendc.web.proto.user.Job {
+ return org.opendc.web.proto.user.Job(id, state, createdAt, updatedAt, results)
+}
+
+/**
+ * Convert a [Workload] entity into a DTO.
+ */
+internal fun Workload.toDto(): org.opendc.web.proto.Workload {
+ return org.opendc.web.proto.Workload(trace.toUserDto(), samplingFraction)
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt
new file mode 100644
index 00000000..254be8b7
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.service
+
+import org.opendc.web.proto.user.ProjectRole
+
+/**
+ * Flag to indicate that the user can edit a project.
+ */
+internal val ProjectRole.canEdit: Boolean
+ get() = when (this) {
+ ProjectRole.OWNER, ProjectRole.EDITOR -> true
+ ProjectRole.VIEWER -> false
+ }
+
+/**
+ * Flag to indicate that the user can delete a project.
+ */
+internal val ProjectRole.canDelete: Boolean
+ get() = this == ProjectRole.OWNER
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt
new file mode 100644
index 00000000..8d91a00c
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import io.quarkus.jackson.ObjectMapperCustomizer
+import javax.inject.Singleton
+
+/**
+ * Helper class to register the Kotlin Jackson module.
+ */
+@Singleton
+class KotlinModuleCustomizer : ObjectMapperCustomizer {
+ override fun customize(objectMapper: ObjectMapper) {
+ objectMapper.registerModule(KotlinModule.Builder().build())
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..134739c9
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueExtractor
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicExtractor
+import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
+import java.sql.CallableStatement
+import java.sql.ResultSet
+import java.sql.Types
+
+/**
+ * Abstract implementation of a [SqlTypeDescriptor] for Hibernate JSON type.
+ */
+internal abstract class AbstractJsonSqlTypeDescriptor : SqlTypeDescriptor {
+
+ override fun getSqlType(): Int {
+ return Types.OTHER
+ }
+
+ override fun canBeRemapped(): Boolean {
+ return true
+ }
+
+ override fun <X> getExtractor(typeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
+ return object : BasicExtractor<X>(typeDescriptor, this) {
+ override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(rs, name), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(statement, index), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(statement, name), options)
+ }
+ }
+ }
+
+ open fun extractJson(rs: ResultSet, name: String): Any? {
+ return rs.getObject(name)
+ }
+
+ open fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return statement.getObject(index)
+ }
+
+ open fun extractJson(statement: CallableStatement, name: String): Any? {
+ return statement.getObject(name)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
new file mode 100644
index 00000000..32f69928
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
@@ -0,0 +1,26 @@
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.JsonNode
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.sql.CallableStatement
+import java.sql.PreparedStatement
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as binary (JSONB).
+ */
+internal object JsonBinarySqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(typeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setObject(index, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setObject(name, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
+ }
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
new file mode 100644
index 00000000..eaecc5b0
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.io.UnsupportedEncodingException
+import java.sql.*
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as UTF-8 encoded bytes.
+ */
+internal object JsonBytesSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ private val CHARSET = Charsets.UTF_8
+
+ override fun getSqlType(): Int {
+ return Types.BINARY
+ }
+
+ override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(javaTypeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setBytes(index, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
+ }
+ }
+ }
+
+ override fun extractJson(rs: ResultSet, name: String): Any? {
+ return fromJsonBytes(rs.getBytes(name))
+ }
+
+ override fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return fromJsonBytes(statement.getBytes(index))
+ }
+
+ override fun extractJson(statement: CallableStatement, name: String): Any? {
+ return fromJsonBytes(statement.getBytes(name))
+ }
+
+ private fun toJsonBytes(jsonValue: String): ByteArray? {
+ return try {
+ jsonValue.toByteArray(CHARSET)
+ } catch (e: UnsupportedEncodingException) {
+ throw IllegalStateException(e)
+ }
+ }
+
+ private fun fromJsonBytes(jsonBytes: ByteArray?): String? {
+ return if (jsonBytes == null) {
+ null
+ } else try {
+ String(jsonBytes, CHARSET)
+ } catch (e: UnsupportedEncodingException) {
+ throw IllegalStateException(e)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..e005f368
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.dialect.H2Dialect
+import org.hibernate.dialect.PostgreSQL81Dialect
+import org.hibernate.internal.SessionImpl
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.ValueExtractor
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import org.hibernate.type.descriptor.sql.BasicExtractor
+import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
+import java.sql.*
+
+/**
+ * A [SqlTypeDescriptor] that automatically selects the correct implementation for the database dialect.
+ */
+internal object JsonSqlTypeDescriptor : SqlTypeDescriptor {
+
+ override fun getSqlType(): Int = Types.OTHER
+
+ override fun canBeRemapped(): Boolean = true
+
+ override fun <X> getExtractor(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
+ return object : BasicExtractor<X>(javaTypeDescriptor, this) {
+ private var delegate: AbstractJsonSqlTypeDescriptor? = null
+
+ override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(rs, name), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, index), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, name), options)
+ }
+
+ private fun delegate(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
+ var delegate = delegate
+ if (delegate == null) {
+ delegate = resolveSqlTypeDescriptor(options)
+ this.delegate = delegate
+ }
+ return delegate
+ }
+ }
+ }
+
+ override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(javaTypeDescriptor, this) {
+ private var delegate: ValueBinder<X>? = null
+
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ delegate(options).bind(st, value, index, options)
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ delegate(options).bind(st, value, name, options)
+ }
+
+ private fun delegate(options: WrapperOptions): ValueBinder<X> {
+ var delegate = delegate
+ if (delegate == null) {
+ delegate = checkNotNull(resolveSqlTypeDescriptor(options).getBinder(javaTypeDescriptor))
+ this.delegate = delegate
+ }
+ return delegate
+ }
+ }
+ }
+
+ /**
+ * Helper method to resolve the appropriate [SqlTypeDescriptor] based on the [WrapperOptions].
+ */
+ private fun resolveSqlTypeDescriptor(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
+ val session = options as? SessionImpl
+ return when (session?.jdbcServices?.dialect) {
+ is PostgreSQL81Dialect -> JsonBinarySqlTypeDescriptor
+ is H2Dialect -> JsonBytesSqlTypeDescriptor
+ else -> JsonStringSqlTypeDescriptor
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
new file mode 100644
index 00000000..cf400c95
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
@@ -0,0 +1,38 @@
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.sql.*
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as string (VARCHAR).
+ */
+internal object JsonStringSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ override fun getSqlType(): Int = Types.VARCHAR
+
+ override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(typeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setString(index, typeDescriptor.unwrap(value, String::class.java, options))
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setString(name, typeDescriptor.unwrap(value, String::class.java, options))
+ }
+ }
+ }
+
+ override fun extractJson(rs: ResultSet, name: String): Any? {
+ return rs.getString(name)
+ }
+
+ override fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return statement.getString(index)
+ }
+
+ override fun extractJson(statement: CallableStatement, name: String): Any? {
+ return statement.getString(name)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt
new file mode 100644
index 00000000..2206e82f
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.hibernate.type.AbstractSingleColumnStandardBasicType
+import org.hibernate.type.BasicType
+import org.hibernate.usertype.DynamicParameterizedType
+import java.util.*
+import javax.enterprise.inject.spi.CDI
+
+/**
+ * A [BasicType] that contains JSON.
+ */
+class JsonType(objectMapper: ObjectMapper) : AbstractSingleColumnStandardBasicType<Any>(JsonSqlTypeDescriptor, JsonTypeDescriptor(objectMapper)), DynamicParameterizedType {
+ /**
+ * No-arg constructor for Hibernate to instantiate.
+ */
+ constructor() : this(CDI.current().select(ObjectMapper::class.java).get())
+
+ override fun getName(): String = "json"
+
+ override fun registerUnderJavaType(): Boolean = true
+
+ override fun setParameterValues(parameters: Properties) {
+ (javaTypeDescriptor as JsonTypeDescriptor).setParameterValues(parameters)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt
new file mode 100644
index 00000000..3386582e
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.hibernate.HibernateException
+import org.hibernate.annotations.common.reflection.XProperty
+import org.hibernate.annotations.common.reflection.java.JavaXMember
+import org.hibernate.engine.jdbc.BinaryStream
+import org.hibernate.engine.jdbc.internal.BinaryStreamImpl
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.AbstractTypeDescriptor
+import org.hibernate.type.descriptor.java.BlobTypeDescriptor
+import org.hibernate.type.descriptor.java.DataHelper
+import org.hibernate.type.descriptor.java.MutableMutabilityPlan
+import org.hibernate.usertype.DynamicParameterizedType
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.lang.reflect.Type
+import java.sql.Blob
+import java.sql.SQLException
+import java.util.*
+
+/**
+ * An [AbstractTypeDescriptor] implementation for Hibernate JSON type.
+ */
+internal class JsonTypeDescriptor(private val objectMapper: ObjectMapper) : AbstractTypeDescriptor<Any>(Any::class.java, JsonMutabilityPlan(objectMapper)), DynamicParameterizedType {
+ private var type: Type? = null
+
+ override fun setParameterValues(parameters: Properties) {
+ val xProperty = parameters[DynamicParameterizedType.XPROPERTY] as XProperty
+ type = if (xProperty is JavaXMember) {
+ val x = xProperty as JavaXMember
+ x.javaType
+ } else {
+ (parameters[DynamicParameterizedType.PARAMETER_TYPE] as DynamicParameterizedType.ParameterType).returnedClass
+ }
+ }
+
+ override fun areEqual(one: Any?, another: Any?): Boolean {
+ return when {
+ one === another -> true
+ one == null || another == null -> false
+ one is String && another is String -> one == another
+ one is Collection<*> && another is Collection<*> -> Objects.equals(one, another)
+ else -> areJsonEqual(one, another)
+ }
+ }
+
+ override fun toString(value: Any?): String {
+ return objectMapper.writeValueAsString(value)
+ }
+
+ override fun fromString(string: String): Any? {
+ return objectMapper.readValue(string, objectMapper.typeFactory.constructType(type))
+ }
+
+ override fun <X> unwrap(value: Any?, type: Class<X>, options: WrapperOptions): X? {
+ if (value == null) {
+ return null
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return when {
+ String::class.java.isAssignableFrom(type) -> toString(value)
+ BinaryStream::class.java.isAssignableFrom(type) || ByteArray::class.java.isAssignableFrom(type) -> {
+ val stringValue = if (value is String) value else toString(value)
+ BinaryStreamImpl(DataHelper.extractBytes(ByteArrayInputStream(stringValue.toByteArray())))
+ }
+ Blob::class.java.isAssignableFrom(type) -> {
+ val stringValue = if (value is String) value else toString(value)
+ BlobTypeDescriptor.INSTANCE.fromString(stringValue)
+ }
+ Any::class.java.isAssignableFrom(type) -> toJsonType(value)
+ else -> throw unknownUnwrap(type)
+ } as X
+ }
+
+ override fun <X> wrap(value: X?, options: WrapperOptions): Any? {
+ if (value == null) {
+ return null
+ }
+
+ var blob: Blob? = null
+ if (Blob::class.java.isAssignableFrom(value.javaClass)) {
+ blob = options.lobCreator.wrap(value as Blob?)
+ } else if (ByteArray::class.java.isAssignableFrom(value.javaClass)) {
+ blob = options.lobCreator.createBlob(value as ByteArray?)
+ } else if (InputStream::class.java.isAssignableFrom(value.javaClass)) {
+ val inputStream = value as InputStream
+ blob = try {
+ options.lobCreator.createBlob(inputStream, inputStream.available().toLong())
+ } catch (e: IOException) {
+ throw unknownWrap(value.javaClass)
+ }
+ }
+
+ val stringValue: String = try {
+ if (blob != null) String(DataHelper.extractBytes(blob.binaryStream)) else value.toString()
+ } catch (e: SQLException) {
+ throw HibernateException("Unable to extract binary stream from Blob", e)
+ }
+
+ return fromString(stringValue)
+ }
+
+ private class JsonMutabilityPlan(private val objectMapper: ObjectMapper) : MutableMutabilityPlan<Any>() {
+ override fun deepCopyNotNull(value: Any): Any {
+ return objectMapper.treeToValue(objectMapper.valueToTree(value), value.javaClass)
+ }
+ }
+
+ private fun readObject(value: String): Any {
+ return objectMapper.readTree(value)
+ }
+
+ private fun areJsonEqual(one: Any, another: Any): Boolean {
+ return readObject(objectMapper.writeValueAsString(one)) == readObject(objectMapper.writeValueAsString(another))
+ }
+
+ private fun toJsonType(value: Any?): Any {
+ return try {
+ readObject(objectMapper.writeValueAsString(value))
+ } catch (e: Exception) {
+ throw IllegalArgumentException(e)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png b/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png
new file mode 100644
index 00000000..d743038b
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png
Binary files differ
diff --git a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties
new file mode 100644
index 00000000..84da528f
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties
@@ -0,0 +1,28 @@
+# Copyright (c) 2022 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.
+
+# Datasource (H2)
+quarkus.datasource.db-kind = h2
+quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS blob;
+
+# Hibernate
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.hibernate-orm.sql-load-script=init-dev.sql
diff --git a/opendc-web/opendc-web-api/src/main/resources/application-prod.properties b/opendc-web/opendc-web-api/src/main/resources/application-prod.properties
new file mode 100644
index 00000000..3af1dfd9
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/application-prod.properties
@@ -0,0 +1,29 @@
+# Copyright (c) 2022 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.
+
+# Datasource
+quarkus.datasource.db-kind=postgresql
+quarkus.datasource.username=${OPENDC_DB_USERNAME}
+quarkus.datasource.password=${OPENDC_DB_PASSWORD}
+quarkus.datasource.jdbc.url=${OPENDC_DB_URL}
+
+# Hibernate
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQL95Dialect
+quarkus.hibernate-orm.database.generation=validate
diff --git a/opendc-web/opendc-web-api/src/main/resources/application-test.properties b/opendc-web/opendc-web-api/src/main/resources/application-test.properties
new file mode 100644
index 00000000..0710f200
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/application-test.properties
@@ -0,0 +1,36 @@
+# Copyright (c) 2022 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.
+
+# Datasource configuration
+quarkus.datasource.db-kind = h2
+quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE "JSONB" AS blob;
+
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.hibernate-orm.database.generation=drop-and-create
+
+# No OIDC for tests
+quarkus.oidc.enabled=false
+quarkus.oidc.auth-server-url=
+quarkus.oidc.client-id=
+
+# Disable OpenAPI/Swagger
+quarkus.smallrye-openapi.enable=false
+quarkus.swagger-ui.enable=false
+quarkus.smallrye-openapi.oidc-open-id-connect-url=
diff --git a/opendc-web/opendc-web-api/src/main/resources/application.properties b/opendc-web/opendc-web-api/src/main/resources/application.properties
new file mode 100644
index 00000000..fa134e7e
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/application.properties
@@ -0,0 +1,48 @@
+# Copyright (c) 2022 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.
+
+quarkus.http.cors=true
+
+# OpenID
+quarkus.oidc.auth-server-url=https://${OPENDC_AUTH0_DOMAIN}
+quarkus.oidc.client-id=${OPENDC_AUTH0_AUDIENCE}
+quarkus.oidc.token.audience=${quarkus.oidc.client-id}
+quarkus.oidc.roles.role-claim-path=scope
+
+# OpenAPI and Swagger
+quarkus.smallrye-openapi.info-title=OpenDC REST API
+%dev.quarkus.smallrye-openapi.info-title=OpenDC REST API (development)
+quarkus.smallrye-openapi.info-version=2.1-rc1
+quarkus.smallrye-openapi.info-description=OpenDC is an open-source datacenter simulator for education, featuring real-time online collaboration, diverse simulation models, and detailed performance feedback statistics.
+quarkus.smallrye-openapi.info-contact-email=opendc@atlarge-research.com
+quarkus.smallrye-openapi.info-contact-name=OpenDC Support
+quarkus.smallrye-openapi.info-contact-url=https://opendc.org
+quarkus.smallrye-openapi.info-license-name=MIT
+quarkus.smallrye-openapi.info-license-url=https://github.com/atlarge-research/opendc/blob/master/LICENSE.txt
+
+quarkus.swagger-ui.path=docs
+quarkus.swagger-ui.always-include=true
+quarkus.swagger-ui.oauth-client-id=${OPENDC_AUTH0_DOCS_CLIENT_ID:}
+quarkus.swagger-ui.oauth-additional-query-string-params={"audience":"${OPENDC_AUTH0_AUDIENCE:https://api.opendc.org/}"}
+
+quarkus.smallrye-openapi.security-scheme=oidc
+quarkus.smallrye-openapi.security-scheme-name=Auth0
+quarkus.smallrye-openapi.oidc-open-id-connect-url=https://${OPENDC_AUTH0_DOMAIN:opendc.eu.auth0.com}/.well-known/openid-configuration
+quarkus.smallrye-openapi.servers=http://localhost:8080
diff --git a/opendc-web/opendc-web-api/src/main/resources/init-dev.sql b/opendc-web/opendc-web-api/src/main/resources/init-dev.sql
new file mode 100644
index 00000000..756eff46
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/resources/init-dev.sql
@@ -0,0 +1,3 @@
+
+-- Add example traces
+INSERT INTO traces (id, name, type) VALUES ('bitbrains-small', 'Bitbrains Small', 'vm');
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt
new file mode 100644
index 00000000..e88e1c1c
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+package org.opendc.web.api.rest
+
+import io.quarkus.test.junit.QuarkusTest
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.junit.jupiter.api.Test
+
+/**
+ * Test suite for [SchedulerResource]
+ */
+@QuarkusTest
+class SchedulerResourceTest {
+ /**
+ * Test to verify whether we can obtain all schedulers.
+ */
+ @Test
+ fun testGetSchedulers() {
+ When {
+ get("/schedulers")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt
new file mode 100644
index 00000000..6fab9953
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.equalTo
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.TraceService
+import org.opendc.web.proto.Trace
+
+/**
+ * Test suite for [TraceResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(TraceResource::class)
+class TraceResourceTest {
+ @InjectMock
+ private lateinit var traceService: TraceService
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(traceService, TraceService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain all traces (empty response).
+ */
+ @Test
+ fun testGetAllEmpy() {
+ every { traceService.findAll() } returns emptyList()
+
+ When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("", Matchers.empty<String>())
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent trace.
+ */
+ @Test
+ fun testGetNonExisting() {
+ every { traceService.findById("bitbrains") } returns null
+
+ When {
+ get("/bitbrains")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain an existing trace.
+ */
+ @Test
+ fun testGetExisting() {
+ every { traceService.findById("bitbrains") } returns Trace("bitbrains", "Bitbrains", "VM")
+
+ When {
+ get("/bitbrains")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("name", equalTo("Bitbrains"))
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt
new file mode 100644
index 00000000..b82c60e8
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.runner
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers.equalTo
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.JobService
+import org.opendc.web.proto.*
+import org.opendc.web.proto.Targets
+import org.opendc.web.proto.runner.Job
+import org.opendc.web.proto.runner.Portfolio
+import org.opendc.web.proto.runner.Scenario
+import org.opendc.web.proto.runner.Topology
+import java.time.Instant
+
+/**
+ * Test suite for [JobResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(JobResource::class)
+class JobResourceTest {
+ @InjectMock
+ private lateinit var jobService: JobService
+
+ /**
+ * Dummy values
+ */
+ private val dummyPortfolio = Portfolio(1, 1, "test", Targets(emptySet()))
+ private val dummyTopology = Topology(1, 1, "test", emptyList(), Instant.now(), Instant.now())
+ private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm")
+ private val dummyScenario = Scenario(1, 1, dummyPortfolio, "test", Workload(dummyTrace, 1.0), dummyTopology, OperationalPhenomena(false, false), "test",)
+ private val dummyJob = Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now())
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(jobService, JobService::class.java)
+ }
+
+ /**
+ * Test that tries to query the pending jobs without token.
+ */
+ @Test
+ fun testQueryWithoutToken() {
+ When {
+ get()
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to query the pending jobs for a user.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testQueryInvalidScope() {
+ When {
+ get()
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to query the pending jobs for a runner.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testQuery() {
+ every { jobService.queryPending() } returns listOf(dummyJob)
+
+ When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("get(0).id", equalTo(1))
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent job.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetNonExisting() {
+ every { jobService.findById(1) } returns null
+
+ When {
+ get("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a job.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetExisting() {
+ every { jobService.findById(1) } returns dummyJob
+
+ When {
+ get("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", equalTo(1))
+ }
+ }
+
+ /**
+ * Test that tries to update a non-existent job.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testUpdateNonExistent() {
+ every { jobService.updateState(1, any(), any()) } returns null
+
+ Given {
+ body(Job.Update(JobState.PENDING))
+ contentType(ContentType.JSON)
+ } When {
+ post("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to update a job.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testUpdateState() {
+ every { jobService.updateState(1, any(), any()) } returns dummyJob.copy(state = JobState.CLAIMED)
+
+ Given {
+ body(Job.Update(JobState.CLAIMED))
+ contentType(ContentType.JSON)
+ } When {
+ post("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("state", equalTo(JobState.CLAIMED.toString()))
+ }
+ }
+
+ /**
+ * Test that tries to update a job with invalid input.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testUpdateInvalidInput() {
+ Given {
+ body("""{ "test": "test" }""")
+ contentType(ContentType.JSON)
+ } When {
+ post("/1")
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt
new file mode 100644
index 00000000..f74efbca
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.PortfolioService
+import org.opendc.web.proto.Targets
+import org.opendc.web.proto.user.Portfolio
+import org.opendc.web.proto.user.Project
+import org.opendc.web.proto.user.ProjectRole
+import java.time.Instant
+
+/**
+ * Test suite for [PortfolioResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(PortfolioResource::class)
+class PortfolioResourceTest {
+ @InjectMock
+ private lateinit var portfolioService: PortfolioService
+
+ /**
+ * Dummy project and portfolio
+ */
+ private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER)
+ private val dummyPortfolio = Portfolio(1, 1, dummyProject, "test", Targets(emptySet(), 1), emptyList())
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(portfolioService, PortfolioService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain the list of portfolios belonging to a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetForProject() {
+ every { portfolioService.findAll("testUser", 1) } returns emptyList()
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a topology for a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateNonExistent() {
+ every { portfolioService.create("testUser", 1, any()) } returns null
+
+ Given {
+ pathParam("project", "1")
+
+ body(Portfolio.Create("test", Targets(emptySet(), 1)))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a portfolio for a scenario.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreate() {
+ every { portfolioService.create("testUser", 1, any()) } returns dummyPortfolio
+
+ Given {
+ pathParam("project", "1")
+
+ body(Portfolio.Create("test", Targets(emptySet(), 1)))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ body("name", Matchers.equalTo("test"))
+ }
+ }
+
+ /**
+ * Test to create a portfolio with an empty body.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateEmpty() {
+ Given {
+ pathParam("project", "1")
+
+ body("{}")
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to create a portfolio with a blank name.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateBlankName() {
+ Given {
+ pathParam("project", "1")
+
+ body(Portfolio.Create("", Targets(emptySet(), 1)))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a portfolio without token.
+ */
+ @Test
+ fun testGetWithoutToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a portfolio with an invalid scope.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetInvalidToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetNonExisting() {
+ every { portfolioService.findOne("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetExisting() {
+ every { portfolioService.findOne("testUser", 1, 1) } returns dummyPortfolio
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ }
+ }
+
+ /**
+ * Test to delete a non-existent portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDeleteNonExistent() {
+ every { portfolioService.delete("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(404)
+ }
+ }
+
+ /**
+ * Test to delete a portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDelete() {
+ every { portfolioService.delete("testUser", 1, 1) } returns dummyPortfolio
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt
new file mode 100644
index 00000000..dbafa8c0
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.ScenarioService
+import org.opendc.web.proto.*
+import org.opendc.web.proto.user.*
+import java.time.Instant
+
+/**
+ * Test suite for [PortfolioScenarioResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(PortfolioScenarioResource::class)
+class PortfolioScenarioResourceTest {
+ @InjectMock
+ private lateinit var scenarioService: ScenarioService
+
+ /**
+ * Dummy values
+ */
+ private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER)
+ private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet()))
+ private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null)
+ private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm")
+ private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now())
+ private val dummyScenario = Scenario(
+ 1,
+ 1,
+ dummyProject,
+ dummyPortfolio,
+ "test",
+ Workload(dummyTrace, 1.0),
+ dummyTopology,
+ OperationalPhenomena(false, false),
+ "test",
+ dummyJob
+ )
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain a portfolio without token.
+ */
+ @Test
+ fun testGetWithoutToken() {
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+ } When {
+ get()
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a portfolio with an invalid scope.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetInvalidToken() {
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+ } When {
+ get()
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGet() {
+ every { scenarioService.findAll("testUser", 1, 1) } returns emptyList()
+
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+ } When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a scenario for a portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateNonExistent() {
+ every { scenarioService.create("testUser", 1, any(), any()) } returns null
+
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+
+ body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test"))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a scenario for a portfolio.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreate() {
+ every { scenarioService.create("testUser", 1, 1, any()) } returns dummyScenario
+
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+
+ body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test"))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ body("name", Matchers.equalTo("test"))
+ }
+ }
+
+ /**
+ * Test to create a project with an empty body.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateEmpty() {
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+
+ body("{}")
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to create a project with a blank name.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateBlankName() {
+ Given {
+ pathParam("project", "1")
+ pathParam("portfolio", "1")
+
+ body(Scenario.Create("", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test"))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt
new file mode 100644
index 00000000..bcbcbab1
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers.equalTo
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.ProjectService
+import org.opendc.web.proto.user.Project
+import org.opendc.web.proto.user.ProjectRole
+import java.time.Instant
+
+/**
+ * Test suite for [ProjectResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(ProjectResource::class)
+class ProjectResourceTest {
+ @InjectMock
+ private lateinit var projectService: ProjectService
+
+ /**
+ * Dummy values.
+ */
+ private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER)
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(projectService, ProjectService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain all projects without token.
+ */
+ @Test
+ fun testGetAllWithoutToken() {
+ When {
+ get()
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to obtain all projects with an invalid scope.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetAllWithInvalidScope() {
+ When {
+ get()
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to obtain all project for a user.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetAll() {
+ val projects = listOf(dummyProject)
+ every { projectService.findWithUser("testUser") } returns projects
+
+ When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("get(0).name", equalTo("test"))
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetNonExisting() {
+ every { projectService.findWithUser("testUser", 1) } returns null
+
+ When {
+ get("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a job.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetExisting() {
+ every { projectService.findWithUser("testUser", 1) } returns dummyProject
+
+ When {
+ get("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", equalTo(0))
+ }
+ }
+
+ /**
+ * Test that tries to create a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreate() {
+ every { projectService.createForUser("testUser", "test") } returns dummyProject
+
+ Given {
+ body(Project.Create("test"))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", equalTo(0))
+ body("name", equalTo("test"))
+ }
+ }
+
+ /**
+ * Test to create a project with an empty body.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateEmpty() {
+ Given {
+ body("{}")
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to create a project with a blank name.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateBlankName() {
+ Given {
+ body(Project.Create(""))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to delete a non-existent project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDeleteNonExistent() {
+ every { projectService.deleteWithUser("testUser", 1) } returns null
+
+ When {
+ delete("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to delete a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDelete() {
+ every { projectService.deleteWithUser("testUser", 1) } returns dummyProject
+
+ When {
+ delete("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to delete a project which the user does not own.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDeleteNonOwner() {
+ every { projectService.deleteWithUser("testUser", 1) } throws IllegalArgumentException("User does not own project")
+
+ When {
+ delete("/1")
+ } Then {
+ statusCode(403)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt
new file mode 100644
index 00000000..65e6e9a1
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.ScenarioService
+import org.opendc.web.proto.*
+import org.opendc.web.proto.user.*
+import java.time.Instant
+
+/**
+ * Test suite for [ScenarioResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(ScenarioResource::class)
+class ScenarioResourceTest {
+ @InjectMock
+ private lateinit var scenarioService: ScenarioService
+
+ /**
+ * Dummy values
+ */
+ private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER)
+ private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet()))
+ private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null)
+ private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm")
+ private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now())
+ private val dummyScenario = Scenario(
+ 1,
+ 1,
+ dummyProject,
+ dummyPortfolio,
+ "test",
+ Workload(dummyTrace, 1.0),
+ dummyTopology,
+ OperationalPhenomena(false, false),
+ "test",
+ dummyJob
+ )
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain a scenario without token.
+ */
+ @Test
+ fun testGetWithoutToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a scenario with an invalid scope.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetInvalidToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent scenario.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetNonExisting() {
+ every { scenarioService.findOne("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a scenario.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetExisting() {
+ every { scenarioService.findOne("testUser", 1, 1) } returns dummyScenario
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ }
+ }
+
+ /**
+ * Test to delete a non-existent scenario.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDeleteNonExistent() {
+ every { scenarioService.delete("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(404)
+ }
+ }
+
+ /**
+ * Test to delete a scenario.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDelete() {
+ every { scenarioService.delete("testUser", 1, 1) } returns dummyScenario
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt
new file mode 100644
index 00000000..ececeaca
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.api.rest.user
+
+import io.mockk.every
+import io.quarkiverse.test.junit.mockk.InjectMock
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusMock
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Given
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.opendc.web.api.service.TopologyService
+import org.opendc.web.proto.user.Project
+import org.opendc.web.proto.user.ProjectRole
+import org.opendc.web.proto.user.Topology
+import java.time.Instant
+
+/**
+ * Test suite for [TopologyResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(TopologyResource::class)
+class TopologyResourceTest {
+ @InjectMock
+ private lateinit var topologyService: TopologyService
+
+ /**
+ * Dummy project and topology.
+ */
+ private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER)
+ private val dummyTopology = Topology(1, 1, dummyProject, "test", emptyList(), Instant.now(), Instant.now())
+
+ @BeforeEach
+ fun setUp() {
+ QuarkusMock.installMockForType(topologyService, TopologyService::class.java)
+ }
+
+ /**
+ * Test that tries to obtain the list of topologies belonging to a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetForProject() {
+ every { topologyService.findAll("testUser", 1) } returns emptyList()
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a topology for a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateNonExistent() {
+ every { topologyService.create("testUser", 1, any()) } returns null
+
+ Given {
+ pathParam("project", "1")
+
+ body(Topology.Create("test", emptyList()))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to create a topology for a project.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreate() {
+ every { topologyService.create("testUser", 1, any()) } returns dummyTopology
+
+ Given {
+ pathParam("project", "1")
+
+ body(Topology.Create("test", emptyList()))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ body("name", Matchers.equalTo("test"))
+ }
+ }
+
+ /**
+ * Test to create a topology with an empty body.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateEmpty() {
+ Given {
+ pathParam("project", "1")
+
+ body("{}")
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to create a topology with a blank name.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testCreateBlankName() {
+ Given {
+ pathParam("project", "1")
+
+ body(Topology.Create("", emptyList()))
+ contentType(ContentType.JSON)
+ } When {
+ post()
+ } Then {
+ statusCode(400)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a topology without token.
+ */
+ @Test
+ fun testGetWithoutToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(401)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a topology with an invalid scope.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["runner"])
+ fun testGetInvalidToken() {
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(403)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a non-existent topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetNonExisting() {
+ every { topologyService.findOne("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(404)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test that tries to obtain a topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testGetExisting() {
+ every { topologyService.findOne("testUser", 1, 1) } returns dummyTopology
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ get("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ body("id", Matchers.equalTo(1))
+ println(extract().asPrettyString())
+ }
+ }
+
+ /**
+ * Test to delete a non-existent topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testUpdateNonExistent() {
+ every { topologyService.update("testUser", any(), any(), any()) } returns null
+
+ Given {
+ pathParam("project", "1")
+ body(Topology.Update(emptyList()))
+ contentType(ContentType.JSON)
+ } When {
+ put("/1")
+ } Then {
+ statusCode(404)
+ }
+ }
+
+ /**
+ * Test to update a topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testUpdate() {
+ every { topologyService.update("testUser", any(), any(), any()) } returns dummyTopology
+
+ Given {
+ pathParam("project", "1")
+ body(Topology.Update(emptyList()))
+ contentType(ContentType.JSON)
+ } When {
+ put("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+
+ /**
+ * Test to delete a non-existent topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDeleteNonExistent() {
+ every { topologyService.delete("testUser", 1, 1) } returns null
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(404)
+ }
+ }
+
+ /**
+ * Test to delete a topology.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testDelete() {
+ every { topologyService.delete("testUser", 1, 1) } returns dummyTopology
+
+ Given {
+ pathParam("project", "1")
+ } When {
+ delete("/1")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/static/schema.yml b/opendc-web/opendc-web-api/static/schema.yml
deleted file mode 100644
index 56cf58e7..00000000
--- a/opendc-web/opendc-web-api/static/schema.yml
+++ /dev/null
@@ -1,1631 +0,0 @@
-openapi: 3.0.0
-info:
- version: 2.1.0
- title: OpenDC REST API v2
- description: OpenDC is an open-source datacenter simulator for education, featuring
- real-time online collaboration, diverse simulation models, and detailed
- performance feedback statistics.
- license:
- name: MIT
- url: https://spdx.org/licenses/MIT
- contact:
- name: Support
- url: https://opendc.org
-servers:
- - url: https://api.opendc.org/v2
-externalDocs:
- description: OpenDC REST API v2
- url: https://api.opendc.com/v2/docs/
-security:
- - auth0:
- - openid
-paths:
- /projects:
- get:
- tags:
- - projects
- description: List Projects of the active user
- responses:
- "200":
- description: Successfully
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Project"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- post:
- tags:
- - projects
- description: Add a Project.
- requestBody:
- content:
- application/json:
- schema:
- properties:
- name:
- type: string
- description: The new Project.
- required: true
- responses:
- "200":
- description: Successfully added Project.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Project"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "/projects/{projectId}":
- get:
- tags:
- - projects
- description: Get this Project.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Project.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Project"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- put:
- tags:
- - projects
- description: Update this Project.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- project:
- $ref: "#/components/schemas/Project"
- description: Project's new properties.
- required: true
- responses:
- "200":
- description: Successfully updated Project.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Project"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from updating Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- delete:
- tags:
- - projects
- description: Delete this project.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully deleted Project.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Project"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from deleting Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/projects/{projectId}/topologies":
- get:
- tags:
- - projects
- description: Get Project Topologies.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Project Topologies.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Topology"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- post:
- tags:
- - projects
- description: Add a Topology.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- topology:
- $ref: "#/components/schemas/Topology"
- description: The new Topology.
- required: true
- responses:
- "200":
- description: Successfully added Topology.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Topology"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/projects/{projectId}/portfolios":
- get:
- tags:
- - projects
- description: Get Project Portfolios.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Project Portfolios.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Portfolio"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- post:
- tags:
- - projects
- description: Add a Portfolio.
- parameters:
- - name: projectId
- in: path
- description: Project's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- topology:
- $ref: "#/components/schemas/Portfolio"
- description: The new Portfolio.
- required: true
- responses:
- "200":
- description: Successfully added Portfolio.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Portfolio"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/topologies/{topologyId}":
- get:
- tags:
- - topologies
- description: Get this Topology.
- parameters:
- - name: topologyId
- in: path
- description: Topology's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Topology.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Topology"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Topology.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Topology not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- put:
- tags:
- - topologies
- description: Update this Topology's name.
- parameters:
- - name: topologyId
- in: path
- description: Topology's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- topology:
- $ref: "#/components/schemas/Topology"
- description: Topology's new properties.
- required: true
- responses:
- "200":
- description: Successfully updated Topology.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Topology"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Project.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Project not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- delete:
- tags:
- - topologies
- description: Delete this Topology.
- parameters:
- - name: topologyId
- in: path
- description: Topology's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully deleted Topology.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Topology"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from deleting Topology.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Topology not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/portfolios/{portfolioId}":
- get:
- tags:
- - portfolios
- description: Get this Portfolio.
- parameters:
- - name: portfolioId
- in: path
- description: Portfolio's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Portfolio.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Portfolio"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Portfolio.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Portfolio not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- put:
- tags:
- - portfolios
- description: Update this Portfolio.
- parameters:
- - name: portfolioId
- in: path
- description: Portfolio's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Portfolio"
- description: Portfolio's new properties.
- required: true
- responses:
- "200":
- description: Successfully updated Portfolio.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Portfolio"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Portfolio.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Portfolio not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- delete:
- tags:
- - portfolios
- description: Delete this Portfolio.
- parameters:
- - name: portfolioId
- in: path
- description: Portfolio's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully deleted Portfolio.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Portfolio"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Portfolio.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Portfolio not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/portfolios/{portfolioId}/scenarios":
- get:
- tags:
- - portfolios
- description: Get Portfolio Scenarios.
- parameters:
- - name: portfolioId
- in: path
- description: Portfolio's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Portfolio Scenarios.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Scenario"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Portfolio.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Portfolio not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- post:
- tags:
- - portfolios
- description: Add a Scenario.
- parameters:
- - name: portfolioId
- in: path
- description: Portfolio's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- topology:
- $ref: "#/components/schemas/Scenario"
- description: The new Scenario.
- required: true
- responses:
- "200":
- description: Successfully added Scenario.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Scenario"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "404":
- description: Portfolio not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "/scenarios/{scenarioId}":
- get:
- tags:
- - scenarios
- description: Get this Scenario.
- parameters:
- - name: scenarioId
- in: path
- description: Scenario's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Scenario.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Scenario"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Scenario.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Scenario not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- 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
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Scenario"
- description: Scenario with new name.
- required: true
- responses:
- "200":
- description: Successfully updated Scenario.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Scenario"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Scenario.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Scenario not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- delete:
- tags:
- - scenarios
- description: Delete this Scenario.
- parameters:
- - name: scenarioId
- in: path
- description: Scenario's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully deleted Scenario.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Scenario"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Scenario.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Scenario not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- /schedulers:
- get:
- tags:
- - simulation
- description: Get all available Schedulers
- responses:
- "200":
- description: Successfully retrieved Schedulers.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Scheduler"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- /traces:
- get:
- tags:
- - simulation
- description: Get all available Traces
- responses:
- "200":
- description: Successfully retrieved Traces.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "/traces/{traceId}":
- get:
- tags:
- - simulation
- description: Get this Trace.
- parameters:
- - name: traceId
- in: path
- description: Trace's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Trace.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Trace"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "404":
- description: Trace not found
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- /prefabs:
- get:
- tags:
- - prefabs
- description: Get all Prefabs the user has rights to view.
- responses:
- "200":
- description: Successfully retrieved prefabs the user is authorized on.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Prefab"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- post:
- tags:
- - prefabs
- description: Add a Prefab.
- requestBody:
- content:
- application/json:
- schema:
- properties:
- name:
- type: string
- description: The new Prefab.
- required: true
- responses:
- "200":
- description: Successfully added Prefab.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Prefab"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "/prefabs/{prefabId}":
- get:
- tags:
- - prefabs
- description: Get this Prefab.
- parameters:
- - name: prefabId
- in: path
- description: Prefab's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Prefab.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Prefab"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Prefab.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Prefab not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- put:
- tags:
- - prefabs
- description: Update this Prefab.
- parameters:
- - name: prefabId
- in: path
- description: Prefab's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- prefab:
- $ref: "#/components/schemas/Prefab"
- description: Prefab's new properties.
- required: true
- responses:
- "200":
- description: Successfully updated Prefab.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Prefab"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Prefab.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Prefab not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- delete:
- tags:
- - prefabs
- description: Delete this prefab.
- parameters:
- - name: prefabId
- in: path
- description: Prefab's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully deleted Prefab.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Prefab"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "404":
- description: Prefab not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- /jobs:
- get:
- tags:
- - jobs
- description: Get all available jobs to run.
- responses:
- "200":
- description: Successfully retrieved available jobs.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/Job"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "/jobs/{jobId}":
- get:
- tags:
- - jobs
- description: Get this Job.
- parameters:
- - name: jobId
- in: path
- description: Job's ID.
- required: true
- schema:
- type: string
- responses:
- "200":
- description: Successfully retrieved Job.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Job"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Job.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Job not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- post:
- tags:
- - jobs
- description: Update this Job.
- parameters:
- - name: jobId
- in: path
- description: Job's ID.
- required: true
- schema:
- type: string
- requestBody:
- content:
- application/json:
- schema:
- properties:
- job:
- $ref: "#/components/schemas/Job"
- description: Job's new properties.
- required: true
- responses:
- "200":
- description: Successfully updated Job.
- content:
- "application/json":
- schema:
- type: object
- required:
- - data
- properties:
- data:
- $ref: "#/components/schemas/Job"
- "400":
- description: Missing or incorrectly typed parameter.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
- "401":
- description: Unauthorized.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Unauthorized"
- "403":
- description: Forbidden from retrieving Job.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Forbidden"
- "404":
- description: Job not found.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/NotFound"
- "409":
- description: State conflict.
- content:
- "application/json":
- schema:
- $ref: "#/components/schemas/Invalid"
-components:
- securitySchemes:
- auth0:
- type: oauth2
- x-token-validation-url: https://opendc.eu.auth0.com/userinfo
- flows:
- authorizationCode:
- authorizationUrl: https://opendc.eu.auth0.com/authorize
- tokenUrl: https://opendc.eu.auth0.com/oauth/token
- scopes:
- openid: Grants access to user_id
- runner: Grants access to runner jobs
- schemas:
- Unauthorized:
- type: object
- required:
- - message
- properties:
- message:
- type: string
- Invalid:
- type: object
- required:
- - message
- - errors
- properties:
- message:
- type: string
- errors:
- type: array
- items:
- type: string
- Forbidden:
- type: object
- required:
- - message
- properties:
- message:
- type: string
- NotFound:
- type: object
- required:
- - message
- properties:
- message:
- type: string
- 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
- authorizations:
- type: array
- items:
- type: object
- properties:
- userId:
- type: string
- level:
- type: string
- enum: ['OWN', 'EDIT', 'VIEW']
- Topology:
- type: object
- properties:
- _id:
- type: string
- projectId:
- type: string
- name:
- type: string
- rooms:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- tiles:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- positionX:
- type: integer
- positionY:
- type: integer
- object:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- capacity:
- type: integer
- powerCapacityW:
- type: integer
- machines:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- position:
- type: integer
- cpus:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- clockRateMhz:
- type: integer
- numberOfCores:
- type: integer
- energyConsumptionW:
- type: integer
- gpus:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- clockRateMhz:
- type: integer
- numberOfCores:
- type: integer
- energyConsumptionW:
- type: integer
- memories:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- speedMbPerS:
- type: integer
- sizeMb:
- type: integer
- energyConsumptionW:
- type: integer
- storages:
- type: array
- items:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- speedMbPerS:
- type: integer
- sizeMb:
- type: integer
- energyConsumptionW:
- 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:
- enabledMetrics:
- type: array
- items:
- type: string
- repeatsPerScenario:
- type: integer
- Scenario:
- type: object
- properties:
- _id:
- type: string
- portfolioId:
- type: string
- name:
- 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
- Job:
- type: object
- properties:
- _id:
- type: string
- scenarioId:
- type: string
- state:
- type: string
- heartbeat:
- type: string
- results:
- type: object
- Trace:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- path:
- type: string
- type:
- type: string
- Prefab:
- type: object
- properties:
- _id:
- type: string
- name:
- type: string
- datetimeCreated:
- type: string
- format: dateTime
- datetimeLastEdited:
- type: string
- format: dateTime
diff --git a/opendc-web/opendc-web-api/tests/api/test_jobs.py b/opendc-web/opendc-web-api/tests/api/test_jobs.py
deleted file mode 100644
index 2efe6933..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_jobs.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# Copyright (c) 2021 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.
-
-#
-# 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:
-#
-#
-from datetime import datetime
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-test_id_2 = 24 * '2'
-
-
-def test_get_jobs(client, mocker):
- mocker.patch.object(db, 'fetch_all', return_value=[
- {'_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}}
- ])
- res = client.get('/jobs/')
- assert '200' in res.status
-
-
-def test_get_job_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/jobs/{test_id}').status
-
-
-def test_get_job(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}
- })
- res = client.get(f'/jobs/{test_id}')
- assert '200' in res.status
-
-
-def test_update_job_nop(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y',
- 'simulation': {'state': 'QUEUED', 'heartbeat': datetime.now()}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'QUEUED'})
- assert '200' in res.status
- update_mock.assert_called_once()
-
-
-def test_update_job_invalid_state(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'})
- assert '400' in res.status
-
-
-def test_update_job_claim(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y',
- 'simulation': {'state': 'CLAIMED', 'heartbeat': datetime.now()}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'})
- assert '200' in res.status
- update_mock.assert_called_once()
-
-
-def test_update_job_conflict(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value=None)
- res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'})
- assert '409' in res.status
- update_mock.assert_called_once()
-
-
-def test_update_job_run(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'CLAIMED'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y',
- 'simulation': {'state': 'RUNNING', 'heartbeat': datetime.now()}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'RUNNING'})
- assert '200' in res.status
- update_mock.assert_called_once()
-
-
-def test_update_job_finished(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y',
- 'simulation': {'state': 'FINISHED', 'heartbeat': datetime.now()}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'})
- assert '200' in res.status
- update_mock.assert_called_once()
-
-
-def test_update_job_failed(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'}
- })
- update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={
- '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y',
- 'simulation': {'state': 'FAILED', 'heartbeat': datetime.now()}
- })
- res = client.post(f'/jobs/{test_id}', json={'state': 'FAILED'})
- assert '200' in res.status
- update_mock.assert_called_once()
diff --git a/opendc-web/opendc-web-api/tests/api/test_portfolios.py b/opendc-web/opendc-web-api/tests/api/test_portfolios.py
deleted file mode 100644
index 196fcb1c..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_portfolios.py
+++ /dev/null
@@ -1,340 +0,0 @@
-# Copyright (c) 2021 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.
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-test_id_2 = 24 * '2'
-
-
-def test_get_portfolio_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/portfolios/{test_id}').status
-
-
-def test_get_portfolio_no_authorizations(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []})
- res = client.get(f'/portfolios/{test_id}')
- assert '403' in res.status
-
-
-def test_get_portfolio_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- 'projectId': test_id,
- '_id': test_id,
- 'authorizations': []
- })
- res = client.get(f'/portfolios/{test_id}')
- assert '403' in res.status
-
-
-def test_get_portfolio(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- 'projectId': test_id,
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- res = client.get(f'/portfolios/{test_id}')
- assert '200' in res.status
-
-
-def test_update_portfolio_missing_parameter(client):
- assert '400' in client.put(f'/portfolios/{test_id}').status
-
-
-def test_update_portfolio_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.put(f'/portfolios/{test_id}', 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': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }]
- })
- mocker.patch.object(db, 'update', return_value={})
- assert '403' in client.put(f'/portfolios/{test_id}', 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': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }],
- 'targets': {
- 'enabledMetrics': [],
- 'repeatsPerScenario': 1
- }
- })
- mocker.patch.object(db, 'update', return_value={})
-
- res = client.put(f'/portfolios/{test_id}', 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(f'/portfolios/{test_id}').status
-
-
-def test_delete_project_different_user(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'googleId': 'other_test',
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }]
- })
- mocker.patch.object(db, 'delete_one', return_value=None)
- assert '403' in client.delete(f'/portfolios/{test_id}').status
-
-
-def test_delete_project(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'googleId': 'test',
- 'portfolioIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- mocker.patch.object(db, 'delete_one', return_value={})
- mocker.patch.object(db, 'update', return_value=None)
- res = client.delete(f'/portfolios/{test_id}')
- assert '200' in res.status
-
-
-def test_add_topology_missing_parameter(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'googleId': 'test',
- 'portfolioIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- assert '400' in client.post(f'/projects/{test_id}/topologies').status
-
-
-def test_add_topology(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }],
- 'topologyIds': []
- })
- mocker.patch.object(db,
- 'insert',
- return_value={
- '_id': test_id,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'topologyIds': []
- })
- mocker.patch.object(db, 'update', return_value={})
- res = client.post(f'/projects/{test_id}/topologies', json={'topology': {'name': 'test project', 'rooms': []}})
- assert 'rooms' in res.json['data']
- assert '200' in res.status
-
-
-def test_add_topology_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }]
- })
- assert '403' in client.post(f'/projects/{test_id}/topologies',
- json={
- 'topology': {
- 'name': 'test_topology',
- 'rooms': []
- }
- }).status
-
-
-def test_add_portfolio_missing_parameter(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'googleId': 'test',
- 'portfolioIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- assert '400' in client.post(f'/projects/{test_id}/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(f'/projects/{test_id}/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': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }]
- })
- assert '403' in client.post(f'/projects/{test_id}/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': test_id,
- 'projectId': test_id,
- 'portfolioIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- mocker.patch.object(db,
- 'insert',
- return_value={
- '_id': test_id,
- 'name': 'test',
- 'targets': {
- 'enabledMetrics': ['test'],
- 'repeatsPerScenario': 2
- },
- 'projectId': test_id,
- 'scenarioIds': [],
- })
- mocker.patch.object(db, 'update', return_value=None)
- res = client.post(
- f'/projects/{test_id}/portfolios',
- json={
- 'portfolio': {
- 'name': 'test',
- 'targets': {
- 'enabledMetrics': ['test'],
- 'repeatsPerScenario': 2
- }
- }
- })
- assert 'projectId' in res.json['data']
- assert 'scenarioIds' in res.json['data']
- assert '200' in res.status
-
-
-def test_get_portfolio_scenarios(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- 'projectId': test_id,
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
- res = client.get(f'/portfolios/{test_id}/scenarios')
- assert '200' in res.status
diff --git a/opendc-web/opendc-web-api/tests/api/test_prefabs.py b/opendc-web/opendc-web-api/tests/api/test_prefabs.py
deleted file mode 100644
index ea3d92d6..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_prefabs.py
+++ /dev/null
@@ -1,252 +0,0 @@
-# Copyright (c) 2021 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.
-
-from unittest.mock import Mock
-from opendc.exts import db
-
-test_id = 24 * '1'
-test_id_2 = 24 * '2'
-
-
-def test_add_prefab_missing_parameter(client):
- assert '400' in client.post('/prefabs/').status
-
-
-def test_add_prefab(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []})
- mocker.patch.object(db,
- 'insert',
- return_value={
- '_id': test_id,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': test_id
- })
- res = client.post('/prefabs/', json={'prefab': {'name': 'test prefab'}})
- assert 'datetimeCreated' in res.json['data']
- assert 'datetimeLastEdited' in res.json['data']
- assert 'authorId' in res.json['data']
- assert '200' in res.status
-
-
-def test_get_prefabs(client, mocker):
- db.fetch_all = Mock()
- mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id})
- db.fetch_all.side_effect = [
- [{
- '_id': test_id,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': test_id,
- 'visibility' : 'private'
- },
- {
- '_id': '2' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': test_id,
- 'visibility' : 'private'
- },
- {
- '_id': '3' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': test_id,
- 'visibility' : 'public'
- },
- {
- '_id': '4' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': test_id,
- 'visibility' : 'public'
- }],
- [{
- '_id': '5' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': '2' * 24,
- 'visibility' : 'public'
- },
- {
- '_id': '6' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': '2' * 24,
- 'visibility' : 'public'
- },
- {
- '_id': '7' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': '2' * 24,
- 'visibility' : 'public'
- },
- {
- '_id': '8' * 24,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'authorId': '2' * 24,
- 'visibility' : 'public'
- }]
- ]
- mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id})
- res = client.get('/prefabs/')
- assert '200' in res.status
-
-
-def test_get_prefab_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/prefabs/{test_id}').status
-
-
-def test_get_private_prefab_not_authorized(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': test_id_2,
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- res = client.get(f'/prefabs/{test_id}')
- assert '403' in res.status
-
-
-def test_get_private_prefab(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': 'test',
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- res = client.get(f'/prefabs/{test_id}')
- assert '200' in res.status
-
-
-def test_get_public_prefab(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': test_id_2,
- 'visibility': 'public',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- res = client.get(f'/prefabs/{test_id}')
- assert '200' in res.status
-
-
-def test_update_prefab_missing_parameter(client):
- assert '400' in client.put(f'/prefabs/{test_id}').status
-
-
-def test_update_prefab_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'S'}}).status
-
-
-def test_update_prefab_not_authorized(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': test_id_2,
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- mocker.patch.object(db, 'update', return_value={})
- assert '403' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}).status
-
-
-def test_update_prefab(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': 'test',
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- mocker.patch.object(db, 'update', return_value={})
- res = client.put(f'/prefabs/{test_id}', 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(f'/prefabs/{test_id}').status
-
-
-def test_delete_prefab_different_user(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': test_id_2,
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- mocker.patch.object(db, 'delete_one', return_value=None)
- assert '403' in client.delete(f'/prefabs/{test_id}').status
-
-
-def test_delete_prefab(client, mocker):
- db.fetch_one = Mock()
- db.fetch_one.side_effect = [{
- '_id': test_id,
- 'name': 'test prefab',
- 'authorId': 'test',
- 'visibility': 'private',
- 'rack': {}
- },
- {
- '_id': test_id
- }
- ]
- mocker.patch.object(db, 'delete_one', return_value={'prefab': {'name': 'name'}})
- res = client.delete(f'/prefabs/{test_id}')
- assert '200' in res.status
diff --git a/opendc-web/opendc-web-api/tests/api/test_projects.py b/opendc-web/opendc-web-api/tests/api/test_projects.py
deleted file mode 100644
index 1cfe4c52..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_projects.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# Copyright (c) 2021 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.
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-
-
-def test_get_user_projects(client, mocker):
- mocker.patch.object(db, 'fetch_all', return_value={'_id': test_id, 'authorizations': [{'userId': 'test',
- 'level': 'OWN'}]})
- res = client.get('/projects/')
- assert '200' in res.status
-
-
-def test_get_user_topologies(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
- res = client.get(f'/projects/{test_id}/topologies')
- assert '200' in res.status
-
-
-def test_get_user_portfolios(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
- res = client.get(f'/projects/{test_id}/portfolios')
- assert '200' in res.status
-
-
-def test_add_project_missing_parameter(client):
- assert '400' in client.post('/projects/').status
-
-
-def test_add_project(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []})
- mocker.patch.object(db,
- 'insert',
- return_value={
- '_id': test_id,
- 'datetimeCreated': '000',
- 'datetimeLastEdited': '000',
- 'topologyIds': []
- })
- mocker.patch.object(db, 'update', return_value={})
- res = client.post('/projects/', json={'project': {'name': 'test project'}})
- assert 'datetimeCreated' in res.json['data']
- assert 'datetimeLastEdited' in res.json['data']
- assert 'topologyIds' in res.json['data']
- assert '200' in res.status
-
-
-def test_get_project_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/projects/{test_id}').status
-
-
-def test_get_project_no_authorizations(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'authorizations': []})
- res = client.get(f'/projects/{test_id}')
- assert '403' in res.status
-
-
-def test_get_project_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': []
- })
- res = client.get(f'/projects/{test_id}')
- assert '403' in res.status
-
-
-def test_get_project(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- res = client.get(f'/projects/{test_id}')
- assert '200' in res.status
-
-
-def test_update_project_missing_parameter(client):
- assert '400' in client.put(f'/projects/{test_id}').status
-
-
-def test_update_project_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status
-
-
-def test_update_project_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }]
- })
- mocker.patch.object(db, 'update', return_value={})
- assert '403' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status
-
-
-def test_update_project(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- mocker.patch.object(db, 'update', return_value={})
-
- res = client.put(f'/projects/{test_id}', 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(f'/projects/{test_id}').status
-
-
-def test_delete_project_different_user(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'googleId': 'other_test',
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'VIEW'
- }],
- 'topologyIds': []
- })
- mocker.patch.object(db, 'delete_one', return_value=None)
- assert '403' in client.delete(f'/projects/{test_id}').status
-
-
-def test_delete_project(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'googleId': 'test',
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }],
- 'topologyIds': [],
- 'portfolioIds': [],
- })
- mocker.patch.object(db, 'update', return_value=None)
- mocker.patch.object(db, 'delete_one', return_value={'googleId': 'test'})
- res = client.delete(f'/projects/{test_id}')
- assert '200' in res.status
diff --git a/opendc-web/opendc-web-api/tests/api/test_scenarios.py b/opendc-web/opendc-web-api/tests/api/test_scenarios.py
deleted file mode 100644
index bdd5c4a3..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_scenarios.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# Copyright (c) 2021 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.
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-test_id_2 = 24 * '2'
-
-
-def test_get_scenario_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/scenarios/{test_id}').status
-
-
-def test_get_scenario_no_authorizations(client, mocker):
- m = mocker.MagicMock()
- m.side_effect = ({'portfolioId': test_id}, {'projectId': test_id}, {'authorizations': []})
- mocker.patch.object(db, 'fetch_one', m)
- res = client.get(f'/scenarios/{test_id}')
- assert '403' in res.status
-
-
-def test_get_scenario(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- side_effect=[
- {'portfolioId': test_id},
- {'projectId': test_id},
- {'authorizations':
- [{'userId': 'test', 'level': 'OWN'}]
- }])
- res = client.get(f'/scenarios/{test_id}')
- assert '200' in res.status
-
-
-def test_update_scenario_missing_parameter(client):
- assert '400' in client.put(f'/scenarios/{test_id}').status
-
-
-def test_update_scenario_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.put(f'/scenarios/{test_id}', json={
- 'scenario': {
- 'name': 'test',
- }
- }).status
-
-
-def test_update_scenario_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- side_effect=[
- {'portfolioId': test_id},
- {'projectId': test_id},
- {'authorizations':
- [{'userId': 'test', 'level': 'VIEW'}]
- }])
- mocker.patch.object(db, 'update', return_value={})
- assert '403' in client.put(f'/scenarios/{test_id}', json={
- 'scenario': {
- 'name': 'test',
- }
- }).status
-
-
-def test_update_scenario(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- side_effect=[
- {'_id': test_id, 'portfolioId': test_id},
- {'projectId': test_id},
- {'authorizations':
- [{'userId': 'test', 'level': 'OWN'}]
- }])
- mocker.patch.object(db, 'update', return_value={})
-
- res = client.put(f'/scenarios/{test_id}', 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(f'/scenarios/{test_id}').status
-
-
-def test_delete_project_different_user(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- side_effect=[
- {'_id': test_id, 'portfolioId': test_id},
- {'projectId': test_id},
- {'authorizations':
- [{'userId': 'test', 'level': 'VIEW'}]
- }])
- mocker.patch.object(db, 'delete_one', return_value=None)
- assert '403' in client.delete(f'/scenarios/{test_id}').status
-
-
-def test_delete_project(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'portfolioId': test_id,
- 'googleId': 'test',
- 'scenarioIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- mocker.patch.object(db, 'delete_one', return_value={})
- mocker.patch.object(db, 'update', return_value=None)
- res = client.delete(f'/scenarios/{test_id}')
- assert '200' in res.status
diff --git a/opendc-web/opendc-web-api/tests/api/test_schedulers.py b/opendc-web/opendc-web-api/tests/api/test_schedulers.py
deleted file mode 100644
index 5d9e6995..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_schedulers.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2021 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.
-
-def test_get_schedulers(client):
- assert '200' in client.get('/schedulers/').status
diff --git a/opendc-web/opendc-web-api/tests/api/test_topologies.py b/opendc-web/opendc-web-api/tests/api/test_topologies.py
deleted file mode 100644
index 6e7c54ef..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_topologies.py
+++ /dev/null
@@ -1,140 +0,0 @@
-# Copyright (c) 2021 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.
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-test_id_2 = 24 * '2'
-
-
-def test_get_topology(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'EDIT'
- }]
- })
- res = client.get(f'/topologies/{test_id}')
- 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('/topologies/1').status
-
-
-def test_get_topology_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': []
- })
- res = client.get(f'/topologies/{test_id}')
- assert '403' in res.status
-
-
-def test_get_topology_no_authorizations(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []})
- res = client.get(f'/topologies/{test_id}')
- assert '403' in res.status
-
-
-def test_update_topology_missing_parameter(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': []
- })
- assert '400' in client.put(f'/topologies/{test_id}').status
-
-
-def test_update_topology_non_existent(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.put(f'/topologies/{test_id}', json={'topology': {'name': 'test_topology', 'rooms': []}}).status
-
-
-def test_update_topology_not_authorized(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': []
- })
- mocker.patch.object(db, 'update', return_value={})
- assert '403' in client.put(f'/topologies/{test_id}', json={
- 'topology': {
- 'name': 'updated_topology',
- 'rooms': []
- }
- }).status
-
-
-def test_update_topology(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- mocker.patch.object(db, 'update', return_value={})
-
- assert '200' in client.put(f'/topologies/{test_id}', json={
- 'topology': {
- 'name': 'updated_topology',
- 'rooms': []
- }
- }).status
-
-
-def test_delete_topology(client, mocker):
- mocker.patch.object(db,
- 'fetch_one',
- return_value={
- '_id': test_id,
- 'projectId': test_id,
- 'googleId': 'test',
- 'topologyIds': [test_id],
- 'authorizations': [{
- 'userId': 'test',
- 'level': 'OWN'
- }]
- })
- mocker.patch.object(db, 'delete_one', return_value={})
- mocker.patch.object(db, 'update', return_value=None)
- res = client.delete(f'/topologies/{test_id}')
- 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(f'/topologies/{test_id}').status
diff --git a/opendc-web/opendc-web-api/tests/api/test_traces.py b/opendc-web/opendc-web-api/tests/api/test_traces.py
deleted file mode 100644
index 0b252c2f..00000000
--- a/opendc-web/opendc-web-api/tests/api/test_traces.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright (c) 2021 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.
-
-from opendc.exts import db
-
-test_id = 24 * '1'
-
-
-def test_get_traces(client, mocker):
- mocker.patch.object(db, 'fetch_all', return_value=[])
- assert '200' in client.get('/traces/').status
-
-
-def test_get_trace_non_existing(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value=None)
- assert '404' in client.get(f'/traces/{test_id}').status
-
-
-def test_get_trace(client, mocker):
- mocker.patch.object(db, 'fetch_one', return_value={'name': 'test trace'})
- res = client.get(f'/traces/{test_id}')
- assert 'name' in res.json['data']
- assert '200' in res.status
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Job.kt b/opendc-web/opendc-web-client/build.gradle.kts
index eeb65e49..f53b29d8 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Job.kt
+++ b/opendc-web/opendc-web-client/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,19 +20,18 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+description = "Client for the OpenDC web API"
-import com.fasterxml.jackson.annotation.JsonProperty
-import java.time.LocalDateTime
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+ `testing-conventions`
+ `jacoco-conventions`
+}
-/**
- * A description of a simulation job.
- */
-public data class Job(
- @JsonProperty("_id")
- val id: String,
- val scenarioId: String,
- val state: SimulationState,
- val heartbeat: LocalDateTime,
- val results: Map<String, Any>
-)
+dependencies {
+ api(projects.opendcWeb.opendcWebProto)
+ implementation(libs.jackson.module.kotlin)
+ implementation(libs.jackson.datatype.jsr310)
+ implementation(libs.jakarta.validation)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt
new file mode 100644
index 00000000..33f2b41e
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.auth.AuthController
+import org.opendc.web.client.transport.HttpTransportClient
+import org.opendc.web.client.transport.TransportClient
+import java.net.URI
+
+/**
+ * Client implementation for the user-facing OpenDC REST API (version 2).
+ *
+ * @param client Low-level client for managing the underlying transport.
+ */
+public class OpenDCClient(client: TransportClient) {
+ /**
+ * Construct a new [OpenDCClient].
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ */
+ public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth))
+
+ /**
+ * A resource for the available projects.
+ */
+ public val projects: ProjectResource = ProjectResource(client)
+
+ /**
+ * A resource for the topologies available to the user.
+ */
+ public val topologies: TopologyResource = TopologyResource(client)
+
+ /**
+ * A resource for the portfolios available to the user.
+ */
+ public val portfolios: PortfolioResource = PortfolioResource(client)
+
+ /**
+ * A resource for the scenarios available to the user.
+ */
+ public val scenarios: ScenarioResource = ScenarioResource(client)
+
+ /**
+ * A resource for the available schedulers.
+ */
+ public val schedulers: SchedulerResource = SchedulerResource(client)
+
+ /**
+ * A resource for the available workload traces.
+ */
+ public val traces: TraceResource = TraceResource(client)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt
new file mode 100644
index 00000000..399804e8
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Portfolio
+
+/**
+ * A resource representing the portfolios available to the user.
+ */
+public class PortfolioResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all portfolios that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Portfolio> = client.get("projects/$project/portfolios") ?: emptyList()
+
+ /**
+ * Obtain the portfolio for [project] with [number].
+ */
+ public fun get(project: Long, number: Int): Portfolio? = client.get("projects/$project/portfolios/$number")
+
+ /**
+ * Create a new portfolio for [project] with the specified [request].
+ */
+ public fun create(project: Long, request: Portfolio.Create): Portfolio {
+ return checkNotNull(client.post("projects/$project/portfolios", request))
+ }
+
+ /**
+ * Delete the portfolio for [project] with [index].
+ */
+ public fun delete(project: Long, index: Int): Portfolio {
+ return requireNotNull(client.delete("projects/$project/portfolios/$index")) { "Unknown portfolio $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt
new file mode 100644
index 00000000..12635b89
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Project
+
+/**
+ * A resource representing the projects available to the user.
+ */
+public class ProjectResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all projects available to the user.
+ */
+ public fun getAll(): List<Project> = client.get("projects") ?: emptyList()
+
+ /**
+ * Obtain the project with [id].
+ */
+ public fun get(id: Long): Project? = client.get("projects/$id")
+
+ /**
+ * Create a new project.
+ */
+ public fun create(name: String): Project = checkNotNull(client.post("projects", Project.Create(name)))
+
+ /**
+ * Delete the project with the specified [id].
+ */
+ public fun delete(id: Long): Project = requireNotNull(client.delete("projects/$id")) { "Unknown project $id" }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt
new file mode 100644
index 00000000..7055e752
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Scenario
+
+/**
+ * A resource representing the scenarios available to the user.
+ */
+public class ScenarioResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all scenarios that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Scenario> = client.get("projects/$project/scenarios") ?: emptyList()
+
+ /**
+ * List all scenarios that belong to the specified [portfolioNumber].
+ */
+ public fun getAll(project: Long, portfolioNumber: Int): List<Scenario> = client.get("projects/$project/portfolios/$portfolioNumber/scenarios") ?: emptyList()
+
+ /**
+ * Obtain the scenario for [project] with [index].
+ */
+ public fun get(project: Long, index: Int): Scenario? = client.get("projects/$project/scenarios/$index")
+
+ /**
+ * Create a new scenario for [portfolio][portfolioNumber] with the specified [request].
+ */
+ public fun create(project: Long, portfolioNumber: Int, request: Scenario.Create): Scenario {
+ return checkNotNull(client.post("projects/$project/portfolios/$portfolioNumber", request))
+ }
+
+ /**
+ * Delete the scenario for [project] with [index].
+ */
+ public fun delete(project: Long, index: Int): Scenario {
+ return requireNotNull(client.delete("projects/$project/scenarios/$index")) { "Unknown scenario $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/AuthConfiguration.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt
index 5dbf2f59..43b72d88 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/AuthConfiguration.kt
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -22,11 +22,15 @@
package org.opendc.web.client
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.transport.TransportClient
+
/**
- * The authentication configuration for the API client.
+ * A resource representing the schedulers available in the OpenDC instance.
*/
-public data class AuthConfiguration(
- val domain: String,
- val clientId: String,
- val clientSecret: String
-)
+public class SchedulerResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all schedulers available.
+ */
+ public fun getAll(): List<String> = client.get("schedulers") ?: emptyList()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt
new file mode 100644
index 00000000..c37ae8da
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.internal.put
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Topology
+
+/**
+ * A resource representing the topologies available to the user.
+ */
+public class TopologyResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all topologies that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Topology> = client.get("projects/$project/topologies") ?: emptyList()
+
+ /**
+ * Obtain the topology for [project] with [index].
+ */
+ public fun get(project: Long, index: Int): Topology? = client.get("projects/$project/topologies/$index")
+
+ /**
+ * Create a new topology for [project] with [request].
+ */
+ public fun create(project: Long, request: Topology.Create): Topology {
+ return checkNotNull(client.post("projects/$project/topologies", request))
+ }
+
+ /**
+ * Update the topology with [index] for [project] using the specified [request].
+ */
+ public fun update(project: Long, index: Int, request: Topology.Update): Topology? {
+ return client.put("projects/$project/topologies/$index", request)
+ }
+
+ /**
+ * Delete the topology for [project] with [index].
+ */
+ public fun delete(project: Long, index: Long): Topology {
+ return requireNotNull(client.delete("projects/$project/topologies/$index")) { "Unknown topology $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt
new file mode 100644
index 00000000..8201c432
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.Trace
+
+/**
+ * A resource representing the workload traces available in the OpenDC instance.
+ */
+public class TraceResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all workload traces available.
+ */
+ public fun getAll(): List<Trace> = client.get("traces") ?: emptyList()
+
+ /**
+ * Obtain the workload trace with the specified [id].
+ */
+ public fun get(id: Long): Trace? = client.get("traces/$id")
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt
new file mode 100644
index 00000000..a4c66f55
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.auth
+
+import java.net.http.HttpRequest
+
+/**
+ * Helper interface for managing API authentication.
+ */
+public interface AuthController {
+ /**
+ * Inject the authorization token into the specified [request].
+ */
+ public fun injectToken(request: HttpRequest.Builder)
+
+ /**
+ * Refresh the current auth token.
+ */
+ public fun refreshToken()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt
new file mode 100644
index 00000000..7f9cbacd
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.auth
+
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.opendc.web.client.internal.OAuthTokenRequest
+import org.opendc.web.client.internal.OAuthTokenResponse
+import org.opendc.web.client.internal.OpenIdConfiguration
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+
+/**
+ * An [AuthController] for OpenID Connect protected APIs.
+ */
+public class OpenIdAuthController(
+ private val domain: String,
+ private val clientId: String,
+ private val clientSecret: String,
+ private val audience: String = "https://api.opendc.org/v2/",
+ private val client: HttpClient = HttpClient.newHttpClient()
+) : AuthController {
+ /**
+ * The Jackson object mapper to convert messages from/to JSON.
+ */
+ private val mapper = jacksonObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ /**
+ * The cached [OpenIdConfiguration].
+ */
+ private val openidConfig: OpenIdConfiguration
+ get() {
+ var openidConfig = _openidConfig
+ if (openidConfig == null) {
+ openidConfig = requestConfig()
+ _openidConfig = openidConfig
+ }
+
+ return openidConfig
+ }
+ private var _openidConfig: OpenIdConfiguration? = null
+
+ /**
+ * The cached OAuth token.
+ */
+ private var _token: OAuthTokenResponse? = null
+
+ override fun injectToken(request: HttpRequest.Builder) {
+ var token = _token
+ if (token == null) {
+ token = requestToken()
+ _token = token
+ }
+
+ request.header("Authorization", "Bearer ${token.accessToken}")
+ }
+
+ /**
+ * Refresh the current access token.
+ */
+ override fun refreshToken() {
+ val refreshToken = _token?.refreshToken
+ if (refreshToken == null) {
+ requestToken()
+ return
+ }
+
+ _token = refreshToken(openidConfig, refreshToken)
+ }
+
+ /**
+ * Request the OpenID configuration from the chosen auth domain
+ */
+ private fun requestConfig(): OpenIdConfiguration {
+ val request = HttpRequest.newBuilder(URI("https://$domain/.well-known/openid-configuration"))
+ .GET()
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Request the auth token from the server.
+ */
+ private fun requestToken(openidConfig: OpenIdConfiguration): OAuthTokenResponse {
+ val body = OAuthTokenRequest.ClientCredentials(audience, clientId, clientSecret)
+ val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Helper method to refresh the auth token.
+ */
+ private fun refreshToken(openidConfig: OpenIdConfiguration, refreshToken: String): OAuthTokenResponse {
+ val body = OAuthTokenRequest.RefreshToken(refreshToken, clientId, clientSecret)
+ val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Fetch a new access token.
+ */
+ private fun requestToken(): OAuthTokenResponse {
+ val token = requestToken(openidConfig)
+ _token = token
+ return token
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt
new file mode 100644
index 00000000..29cf09dc
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.core.type.TypeReference
+import org.opendc.web.client.transport.TransportClient
+
+/**
+ * Perform a GET request for resource at [path] and convert to type [T].
+ */
+internal inline fun <reified T> TransportClient.get(path: String): T? {
+ return get(path, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a POST request for resource at [path] and convert to type [T].
+ */
+internal inline fun <B, reified T> TransportClient.post(path: String, body: B): T? {
+ return post(path, body, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a PUT request for resource at [path] and convert to type [T].
+ */
+internal inline fun <B, reified T> TransportClient.put(path: String, body: B): T? {
+ return put(path, body, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a DELETE request for resource at [path] and convert to type [T].
+ */
+internal inline fun <reified T> TransportClient.delete(path: String): T? {
+ return delete(path, object : TypeReference<T>() {})
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt
new file mode 100644
index 00000000..25341995
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+
+/**
+ * Token request sent to the OAuth server.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "grant_type")
+@JsonSubTypes(
+ value = [
+ JsonSubTypes.Type(value = OAuthTokenRequest.ClientCredentials::class, name = "client_credentials"),
+ JsonSubTypes.Type(value = OAuthTokenRequest.RefreshToken::class, name = "refresh_token")
+ ]
+)
+internal sealed class OAuthTokenRequest {
+ /**
+ * Client credentials grant for OAuth2
+ */
+ data class ClientCredentials(
+ val audience: String,
+ @JsonProperty("client_id")
+ val clientId: String,
+ @JsonProperty("client_secret")
+ val clientSecret: String
+ ) : OAuthTokenRequest()
+
+ /**
+ * Refresh token grant for OAuth2.
+ */
+ data class RefreshToken(
+ @JsonProperty("refresh_token")
+ val refreshToken: String,
+ @JsonProperty("client_id")
+ val clientId: String,
+ @JsonProperty("client_secret")
+ val clientSecret: String
+ ) : OAuthTokenRequest()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt
new file mode 100644
index 00000000..cd5ccab0
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+/**
+ * Token response from the OAuth server.
+ */
+internal data class OAuthTokenResponse(
+ @JsonProperty("access_token")
+ val accessToken: String,
+ @JsonProperty("refresh_token")
+ val refreshToken: String? = null,
+ @JsonProperty("token_type")
+ val tokenType: String,
+ val scope: String = "",
+ @JsonProperty("expires_in")
+ val expiresIn: Long
+)
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt
new file mode 100644
index 00000000..23fbf368
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.net.URI
+
+/**
+ * OpenID configuration exposed by the auth server.
+ */
+internal data class OpenIdConfiguration(
+ val issuer: String,
+ @JsonProperty("authorization_endpoint")
+ val authorizationEndpoint: URI,
+ @JsonProperty("token_endpoint")
+ val tokenEndpoint: URI,
+ @JsonProperty("userinfo_endpoint")
+ val userInfoEndpoint: URI,
+ @JsonProperty("jwks_uri")
+ val jwksUri: URI,
+ @JsonProperty("scopes_supported")
+ val scopesSupported: Set<String>
+)
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt
new file mode 100644
index 00000000..372a92d7
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.runner
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.runner.Job
+
+/**
+ * A resource representing the available simulation jobs for the runner.
+ */
+public class JobResource internal constructor(private val client: TransportClient) {
+ /**
+ * Query the pending jobs.
+ */
+ public fun queryPending(): List<Job> = client.get("jobs") ?: emptyList()
+
+ /**
+ * Obtain the job with [id].
+ */
+ public fun get(id: Long): Job? = client.get("jobs/$id")
+
+ /**
+ * Update the job with [id].
+ */
+ public fun update(id: Long, update: Job.Update): Job? = client.post("jobs/$id", update)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt
new file mode 100644
index 00000000..a3cff6c3
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.runner
+
+import org.opendc.web.client.*
+import org.opendc.web.client.auth.AuthController
+import org.opendc.web.client.transport.HttpTransportClient
+import org.opendc.web.client.transport.TransportClient
+import java.net.URI
+
+/**
+ * Client implementation for the runner-facing OpenDC REST API (version 2).
+ *
+ * @param client Low-level client for managing the underlying transport.
+ */
+public class OpenDCRunnerClient(client: TransportClient) {
+ /**
+ * Construct a new [OpenDCRunnerClient].
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ */
+ public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth))
+
+ /**
+ * A resource for the available simulation jobs.
+ */
+ public val jobs: JobResource = JobResource(client)
+
+ /**
+ * A resource for the available schedulers.
+ */
+ public val schedulers: SchedulerResource = SchedulerResource(client)
+
+ /**
+ * A resource for the available workload traces.
+ */
+ public val traces: TraceResource = TraceResource(client)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt
new file mode 100644
index 00000000..03b3945f
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.transport
+
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import org.opendc.web.client.auth.AuthController
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.nio.file.Paths
+
+/**
+ * A [TransportClient] that accesses the OpenDC API over HTTP.
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ * @param client The HTTP client to use.
+ */
+public class HttpTransportClient(
+ private val baseUrl: URI,
+ private val auth: AuthController,
+ private val client: HttpClient = HttpClient.newHttpClient()
+) : TransportClient {
+ /**
+ * The Jackson object mapper to convert messages from/to JSON.
+ */
+ private val mapper = jacksonObjectMapper()
+ .registerModule(JavaTimeModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ /**
+ * Obtain a resource at [path] of [targetType].
+ */
+ override fun <T> get(path: String, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .GET()
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ get(path, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Update a resource at [path] of [targetType].
+ */
+ override fun <B, T> post(path: String, body: B, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .header("Content-Type", "application/json")
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ post(path, body, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Replace a resource at [path] of [targetType].
+ */
+ override fun <B, T> put(path: String, body: B, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .PUT(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .header("Content-Type", "application/json")
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ put(path, body, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Delete a resource at [path] of [targetType].
+ */
+ override fun <T> delete(path: String, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .DELETE()
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ delete(path, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Build the absolute [URI] to which the request should be sent.
+ */
+ private fun buildUri(path: String): URI = baseUrl.resolve(Paths.get(baseUrl.path, path).toString())
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt
new file mode 100644
index 00000000..af727ca7
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.client.transport
+
+import com.fasterxml.jackson.core.type.TypeReference
+
+/**
+ * Low-level interface for dealing with the transport layer of the API.
+ */
+public interface TransportClient {
+ /**
+ * Obtain a resource at [path] of [targetType].
+ */
+ public fun <T> get(path: String, targetType: TypeReference<T>): T?
+
+ /**
+ * Update a resource at [path] of [targetType].
+ */
+ public fun <B, T> post(path: String, body: B, targetType: TypeReference<T>): T?
+
+ /**
+ * Replace a resource at [path] of [targetType].
+ */
+ public fun <B, T> put(path: String, body: B, targetType: TypeReference<T>): T?
+
+ /**
+ * Delete a resource at [path] of [targetType].
+ */
+ public fun <T> delete(path: String, targetType: TypeReference<T>): T?
+}
diff --git a/opendc-web/opendc-web-ui/src/api/prefabs.js b/opendc-web/opendc-web-proto/build.gradle.kts
index eb9aa23c..4a566346 100644
--- a/opendc-web/opendc-web-ui/src/api/prefabs.js
+++ b/opendc-web/opendc-web-proto/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * 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
@@ -20,20 +20,20 @@
* SOFTWARE.
*/
-import { request } from './index'
+description = "Web communication protocol for OpenDC"
-export function getPrefab(auth, prefabId) {
- return request(auth, `prefabs/${prefabId}`)
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+ id("org.kordamp.gradle.jandex") // Necessary for Quarkus to process annotations
}
-export function addPrefab(auth, prefab) {
- return request(auth, 'prefabs/', 'POST', { prefab })
+dependencies {
+ implementation(libs.jackson.annotations)
+ implementation(libs.jakarta.validation)
+ implementation(libs.microprofile.openapi.api)
}
-export function updatePrefab(auth, prefab) {
- return request(auth, `prefabs/${prefab._id}`, 'PUT', { prefab })
-}
-
-export function deletePrefab(auth, prefabId) {
- return request(auth, `prefabs/${prefabId}`, 'DELETE')
+tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
+ kotlinOptions.javaParameters = true
}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/JobState.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/JobState.kt
new file mode 100644
index 00000000..38b8ca42
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/JobState.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto
+
+/**
+ * State of a scenario for the simulator runner.
+ */
+public enum class JobState {
+ /**
+ * The job is pending to be claimed by a runner.
+ */
+ PENDING,
+
+ /**
+ * The job is claimed by a runner.
+ */
+ CLAIMED,
+
+ /**
+ * The job is currently running.
+ */
+ RUNNING,
+
+ /**
+ * The job has finished.
+ */
+ FINISHED,
+
+ /**
+ * The job has failed.
+ */
+ FAILED;
+}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Machine.kt
index 86d2d46f..f5c50cc3 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Machine.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,17 +20,14 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
/**
* A machine in a rack.
*/
-@JsonIgnoreProperties("id_legacy")
public data class Machine(
- @JsonProperty("_id")
val id: String,
val position: Int,
val cpus: List<ProcessingUnit> = emptyList(),
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/MemoryUnit.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/MemoryUnit.kt
index 11e794e8..1fc604fa 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/MemoryUnit.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/MemoryUnit.kt
@@ -20,15 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
* A memory unit in a system.
*/
public data class MemoryUnit(
- @JsonProperty("_id")
val id: String,
val name: String,
val speedMbPerS: Double,
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/OperationalPhenomena.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/OperationalPhenomena.kt
index ef5b4902..f3164f64 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/OperationalPhenomena.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/OperationalPhenomena.kt
@@ -20,13 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto
/**
* Object describing the enabled operational phenomena for a scenario.
*/
public data class OperationalPhenomena(
- val failuresEnabled: Boolean,
- val performanceInterferenceEnabled: Boolean,
- val schedulerName: String
+ val failures: Boolean,
+ val interference: Boolean
)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ProcessingUnit.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProcessingUnit.kt
index 449b5c43..5f79d1bd 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ProcessingUnit.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProcessingUnit.kt
@@ -20,15 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
* A CPU model.
*/
public data class ProcessingUnit(
- @JsonProperty("_id")
val id: String,
val name: String,
val clockRateMhz: Double,
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTopology.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProtocolError.kt
index 2b90f7ef..e7fe2702 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTopology.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/ProtocolError.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,9 +20,9 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto
/**
- * The topology details for a scenario.
+ * Container for reporting errors.
*/
-public data class ScenarioTopology(val topologyId: String)
+public data class ProtocolError(val code: Int, val message: String)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Rack.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Rack.kt
index a0464388..131aa184 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Rack.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Rack.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,17 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
* A rack in a datacenter.
*/
-@JsonIgnoreProperties("id_legacy")
-public class Rack(
- @JsonProperty("_id")
+public data class Rack(
val id: String,
val name: String,
val capacity: Int,
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Room.kt
index f1b8f946..5b305168 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Room.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,17 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
* A room in a datacenter.
*/
-@JsonIgnoreProperties("id_legacy")
public data class Room(
- @JsonProperty("_id")
val id: String,
val name: String,
val tiles: Set<RoomTile>,
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/RoomTile.kt
index 0b956262..666d66ee 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/RoomTile.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,17 +20,12 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
* A room tile.
*/
-@JsonIgnoreProperties("id_legacy")
public data class RoomTile(
- @JsonProperty("_id")
val id: String,
val positionX: Double,
val positionY: Double,
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/PortfolioTargets.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Targets.kt
index 07c11c19..a0100f72 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/PortfolioTargets.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Targets.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,9 +20,18 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto
+
+import javax.validation.constraints.Min
/**
* The targets of a portfolio.
+ *
+ * @param metrics The selected metrics to track during simulation.
+ * @param repeats The number of repetitions per scenario.
*/
-public data class PortfolioTargets(val enabledMetrics: Set<String>, val repeatsPerScenario: Int)
+public data class Targets(
+ val metrics: Set<String>,
+ @field:Min(1)
+ val repeats: Int = 1
+)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Portfolio.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Trace.kt
index 6904920b..2952a273 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Portfolio.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Trace.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,19 +20,17 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
-
-import com.fasterxml.jackson.annotation.JsonProperty
+package org.opendc.web.proto
/**
- * A portfolio in OpenDC.
+ * A workload trace available for simulation.
+ *
+ * @param id The unique identifier of the trace.
+ * @param name The name of the trace.
+ * @param type The type of trace.
*/
-public data class Portfolio(
- @JsonProperty("_id")
+public data class Trace(
val id: String,
- val projectId: String,
val name: String,
- @JsonProperty("scenarioIds")
- val scenarios: Set<String>,
- val targets: PortfolioTargets
+ val type: String,
)
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Workload.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Workload.kt
new file mode 100644
index 00000000..cc6e0ed8
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/Workload.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+package org.opendc.web.proto
+
+import javax.validation.constraints.DecimalMax
+import javax.validation.constraints.DecimalMin
+
+/**
+ * The workload to simulate for a scenario.
+ */
+public data class Workload(val trace: Trace, val samplingFraction: Double) {
+ /**
+ * Specification for a workload.
+ *
+ * @param trace The unique identifier of the trace.
+ * @param samplingFraction The fraction of the workload to sample.
+ */
+ public data class Spec(
+ val trace: String,
+ @DecimalMin(value = "0.001", message = "Sampling fraction must be non-zero")
+ @DecimalMax(value = "1", message = "Sampling fraction cannot exceed one")
+ val samplingFraction: Double
+ )
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt
new file mode 100644
index 00000000..dfaaa09e
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.runner
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.JobState
+import java.time.Instant
+
+/**
+ * A simulation job to be simulated by a runner.
+ */
+@Schema(name = "Runner.Job")
+public data class Job(
+ val id: Long,
+ val scenario: Scenario,
+ val state: JobState,
+ val createdAt: Instant,
+ val updatedAt: Instant,
+ val results: Map<String, Any>? = null
+) {
+ /**
+ * A request to update the state of a job.
+ */
+ @Schema(name = "Runner.Job.Update")
+ public data class Update(val state: JobState, val results: Map<String, Any>? = null)
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Portfolio.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Portfolio.kt
new file mode 100644
index 00000000..916d8cf0
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Portfolio.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.runner
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.Targets
+import org.opendc.web.proto.user.Portfolio
+
+/**
+ * A [Portfolio] as seen from the runner's perspective.
+ *
+ * @param id The unique identifier of the portfolio.
+ * @param number The number of the portfolio for the project.
+ * @param name The name of the portfolio.
+ * @param targets The targets of the portfolio.
+ */
+@Schema(name = "Runner.Portfolio")
+public data class Portfolio(
+ val id: Long,
+ val number: Int,
+ val name: String,
+ val targets: Targets,
+)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Scenario.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Scenario.kt
index 851ff980..c5e609ec 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Scenario.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Scenario.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,20 +20,22 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto.runner
-import com.fasterxml.jackson.annotation.JsonProperty
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.*
/**
- * A simulation scenario.
+ * A [Scenario] that is exposed to an OpenDC runner.
*/
+@Schema(name = "Runner.Scenario")
public data class Scenario(
- @JsonProperty("_id")
- val id: String,
- val portfolioId: String,
+ val id: Long,
+ val number: Int,
+ val portfolio: Portfolio,
val name: String,
- val trace: ScenarioTrace,
- val topology: ScenarioTopology,
- @JsonProperty("operational")
- val operationalPhenomena: OperationalPhenomena
+ val workload: Workload,
+ val topology: Topology,
+ val phenomena: OperationalPhenomena,
+ val schedulerName: String
)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Topology.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Topology.kt
index b59aba42..ea576e71 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Topology.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Topology.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 AtLarge Research
+ * Copyright (c) 2022 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
@@ -20,19 +20,21 @@
* SOFTWARE.
*/
-package org.opendc.web.client.model
+package org.opendc.web.proto.runner
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
-import com.fasterxml.jackson.annotation.JsonProperty
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.*
+import java.time.Instant
/**
- * Model for an OpenDC topology.
+ * A [Topology] that is exposed to an OpenDC runner.
*/
-@JsonIgnoreProperties("id_legacy", "datacenter_id_legacy", "datetimeLastUpdated", "datetimeLastEdited")
+@Schema(name = "Runner.Topology")
public data class Topology(
- @JsonProperty("_id")
- val id: String,
- val projectId: String,
+ val id: Long,
+ val number: Int,
val name: String,
- val rooms: Set<Room>,
+ val rooms: List<Room>,
+ val createdAt: Instant,
+ val updatedAt: Instant,
)
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Job.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Job.kt
new file mode 100644
index 00000000..de5f8de3
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Job.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+import org.opendc.web.proto.JobState
+import org.opendc.web.proto.runner.Job
+import java.time.Instant
+
+/**
+ * A simulation job that is associated with a [Scenario].
+ *
+ * This entity is exposed in the runner-facing API via [Job].
+ */
+public data class Job(
+ val id: Long,
+ val state: JobState,
+ val createdAt: Instant,
+ val updatedAt: Instant,
+ val results: Map<String, Any>? = null
+)
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Portfolio.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Portfolio.kt
new file mode 100644
index 00000000..6f468e79
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Portfolio.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.Targets
+import javax.validation.constraints.NotBlank
+
+/**
+ * A portfolio is the composition of multiple scenarios.
+ *
+ * @param id The unique identifier of the portfolio.
+ * @param number The number of the portfolio with respect to the project.
+ * @param project The project to which the portfolio belongs.
+ * @param name The name of the portfolio.
+ * @param targets The targets of the portfolio.
+ * @param scenarios The scenarios in the portfolio.
+ */
+public data class Portfolio(
+ val id: Long,
+ val number: Int,
+ val project: Project,
+ val name: String,
+ val targets: Targets,
+ val scenarios: List<Scenario.Summary>
+) {
+ /**
+ * A request to create a new portfolio.
+ */
+ @Schema(name = "Portfolio.Update")
+ public data class Create(
+ @field:NotBlank(message = "Name must not be empty")
+ val name: String,
+ val targets: Targets
+ )
+
+ /**
+ * A summary view of a [Portfolio] provided for nested relations.
+ *
+ * @param id The unique identifier of the portfolio.
+ * @param number The number of the portfolio for the project.
+ * @param name The name of the portfolio.
+ * @param targets The targets of the portfolio.
+ */
+ @Schema(name = "Portfolio.Summary")
+ public data class Summary(
+ val id: Long,
+ val number: Int,
+ val name: String,
+ val targets: Targets,
+ )
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Project.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Project.kt
new file mode 100644
index 00000000..3a2807ca
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Project.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import java.time.Instant
+import javax.validation.constraints.NotBlank
+
+/**
+ * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users.
+ */
+public data class Project(
+ val id: Long,
+ val name: String,
+ val createdAt: Instant,
+ val updatedAt: Instant,
+ val role: ProjectRole
+) {
+ /**
+ * A request to create a new project.
+ */
+ @Schema(name = "Project.Create")
+ public data class Create(@field:NotBlank(message = "Name must not be empty") val name: String)
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/ProjectRole.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/ProjectRole.kt
new file mode 100644
index 00000000..0f6de1fc
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/ProjectRole.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+/**
+ * The role of a user in a project.
+ */
+public enum class ProjectRole {
+ /**
+ * The user is allowed to view the project.
+ */
+ VIEWER,
+
+ /**
+ * The user is allowed to edit the project.
+ */
+ EDITOR,
+
+ /**
+ * The user owns the project (so he can delete it).
+ */
+ OWNER,
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt
new file mode 100644
index 00000000..552a4912
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.OperationalPhenomena
+import org.opendc.web.proto.Workload
+import javax.validation.constraints.NotBlank
+
+/**
+ * A single scenario to be explored by the simulator.
+ */
+public data class Scenario(
+ val id: Long,
+ val number: Int,
+ val project: Project,
+ val portfolio: Portfolio.Summary,
+ val name: String,
+ val workload: Workload,
+ val topology: Topology.Summary,
+ val phenomena: OperationalPhenomena,
+ val schedulerName: String,
+ val job: Job
+) {
+ /**
+ * Create a new scenario.
+ *
+ * @param name The name of the scenario.
+ * @param workload The workload specification to use for the scenario.
+ * @param topology The number of the topology to use.
+ * @param phenomena The phenomena to model during simulation.
+ * @param schedulerName The name of the scheduler.
+ */
+ @Schema(name = "Scenario.Create")
+ public data class Create(
+ @field:NotBlank(message = "Name must not be empty")
+ val name: String,
+ val workload: Workload.Spec,
+ val topology: Long,
+ val phenomena: OperationalPhenomena,
+ val schedulerName: String,
+ )
+
+ /**
+ * A summary view of a [Scenario] provided for nested relations.
+ *
+ * @param id The unique identifier of the scenario.
+ * @param number The number of the scenario for the project.
+ * @param name The name of the scenario.
+ * @param workload The workload to be modeled by the scenario.
+ * @param phenomena The phenomena simulated for this scenario.
+ * @param schedulerName The scheduler name to use for the experiment.
+ * @param job The simulation job associated with the scenario.
+ */
+ @Schema(name = "Scenario.Summary")
+ public data class Summary(
+ val id: Long,
+ val number: Int,
+ val name: String,
+ val workload: Workload,
+ val topology: Topology.Summary,
+ val phenomena: OperationalPhenomena,
+ val schedulerName: String,
+ val job: Job
+ )
+}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Topology.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Topology.kt
new file mode 100644
index 00000000..a144a2e6
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Topology.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+package org.opendc.web.proto.user
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema
+import org.opendc.web.proto.Room
+import java.time.Instant
+import javax.validation.constraints.NotBlank
+
+/**
+ * Model for an OpenDC topology.
+ */
+public data class Topology(
+ val id: Long,
+ val number: Int,
+ val project: Project,
+ val name: String,
+ val rooms: List<Room>,
+ val createdAt: Instant,
+ val updatedAt: Instant,
+) {
+ /**
+ * Create a new topology for a project.
+ */
+ @Schema(name = "Topology.Create")
+ public data class Create(
+ @field:NotBlank(message = "Name must not be empty")
+ val name: String,
+ val rooms: List<Room>
+ )
+
+ /**
+ * Update an existing topology.
+ */
+ @Schema(name = "Topology.Update")
+ public data class Update(val rooms: List<Room>)
+
+ /**
+ * A summary view of a [Topology] provided for nested relations.
+ *
+ * @param id The unique identifier of the topology.
+ * @param number The number of the topology for the project.
+ * @param name The name of the topology.
+ * @param createdAt The instant at which the topology was created.
+ * @param updatedAt The instant at which the topology was updated.
+ */
+ @Schema(name = "Topology.Summary")
+ public data class Summary(
+ val id: Long,
+ val number: Int,
+ val name: String,
+ val createdAt: Instant,
+ val updatedAt: Instant,
+ )
+}
diff --git a/Dockerfile b/opendc-web/opendc-web-runner/Dockerfile
index 771ed2ed..771ed2ed 100644
--- a/Dockerfile
+++ b/opendc-web/opendc-web-runner/Dockerfile
diff --git a/opendc-web/opendc-web-runner/build.gradle.kts b/opendc-web/opendc-web-runner/build.gradle.kts
index a73ca6b3..3c80f605 100644
--- a/opendc-web/opendc-web-runner/build.gradle.kts
+++ b/opendc-web/opendc-web-runner/build.gradle.kts
@@ -40,18 +40,13 @@ dependencies {
implementation(projects.opendcTelemetry.opendcTelemetrySdk)
implementation(projects.opendcTelemetry.opendcTelemetryCompute)
implementation(projects.opendcTrace.opendcTraceApi)
+ implementation(projects.opendcWeb.opendcWebClient)
implementation(libs.kotlin.logging)
implementation(libs.clikt)
implementation(libs.sentry.log4j2)
- implementation(libs.ktor.client.cio)
- implementation(libs.ktor.client.auth)
- implementation(libs.ktor.client.jackson)
- implementation(libs.jackson.datatype.jsr310)
implementation(kotlin("reflect"))
runtimeOnly(projects.opendcTrace.opendcTraceOpendc)
runtimeOnly(libs.log4j.slf4j)
-
- testImplementation(libs.ktor.client.mock)
}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiClient.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiClient.kt
deleted file mode 100644
index 9f2656c4..00000000
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/ApiClient.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (c) 2021 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.
- */
-
-package org.opendc.web.client
-
-import com.fasterxml.jackson.annotation.JsonProperty
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import io.ktor.client.*
-import io.ktor.client.features.auth.*
-import io.ktor.client.features.auth.providers.*
-import io.ktor.client.features.json.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import org.opendc.web.client.model.*
-import java.net.URI
-
-/**
- * Client implementation for the OpenDC REST API (version 2).
- *
- * @param baseUrl The base url of the API.
- * @param auth The authentication configuration for the client.
- * @param client The HTTP client to use.
- */
-public class ApiClient(
- private val baseUrl: URI,
- private val auth: AuthConfiguration,
- private val audience: String = "https://api.opendc.org/v2/",
- client: HttpClient = HttpClient {}
-) : AutoCloseable {
- /**
- * The Ktor [HttpClient] that is used to communicate with the REST API.
- */
- private val client = client.config {
- install(JsonFeature) {
- serializer = JacksonSerializer {
- registerModule(JavaTimeModule())
- }
- }
- install(Auth) {
- bearer {
- loadTokens { requestToken() }
- refreshTokens { requestToken() }
- }
- }
- expectSuccess = false
- }
-
- /**
- * Retrieve the topology with the specified [id].
- */
- public suspend fun getPortfolio(id: String): Portfolio? {
- val url = URLBuilder(Url(baseUrl))
- .path("portfolios", id)
- .build()
- return when (val result = client.get<ApiResult<Portfolio>>(url)) {
- is ApiResult.Success -> result.data
- else -> null
- }
- }
-
- /**
- * Retrieve the scenario with the specified [id].
- */
- public suspend fun getScenario(id: String): Scenario? {
- val url = URLBuilder(Url(baseUrl))
- .path("scenarios", id)
- .build()
- return when (val result = client.get<ApiResult<Scenario>>(url)) {
- is ApiResult.Success -> result.data
- else -> null
- }
- }
-
- /**
- * Retrieve the topology with the specified [id].
- */
- public suspend fun getTopology(id: String): Topology? {
- val url = URLBuilder(Url(baseUrl))
- .path("topologies", id)
- .build()
- return when (val result = client.get<ApiResult<Topology>>(url)) {
- is ApiResult.Success -> result.data
- else -> null
- }
- }
-
- /**
- * Retrieve the available jobs.
- */
- public suspend fun getJobs(): List<Job> {
- val url = URLBuilder(Url(baseUrl))
- .path("jobs")
- .build()
- return when (val result = client.get<ApiResult<List<Job>>>(url)) {
- is ApiResult.Success -> result.data
- else -> emptyList()
- }
- }
-
- /**
- * Update the specified job.
- *
- * @param id The identifier of the job.
- * @param state The new state of the job.
- * @param results The results of the job.
- */
- public suspend fun updateJob(id: String, state: SimulationState, results: Map<String, Any> = emptyMap()): Boolean {
- val url = URLBuilder(Url(baseUrl))
- .path("jobs", id)
- .build()
-
- data class Request(
- val state: SimulationState,
- val results: Map<String, Any>
- )
-
- val res = client.post<HttpResponse> {
- url(url)
- contentType(ContentType.Application.Json)
- body = Request(state, results)
- }
- return res.status.isSuccess()
- }
-
- /**
- * Request the auth token for the API.
- */
- private suspend fun requestToken(): BearerTokens {
- data class Request(
- val audience: String,
- @JsonProperty("grant_type")
- val grantType: String,
- @JsonProperty("client_id")
- val clientId: String,
- @JsonProperty("client_secret")
- val clientSecret: String
- )
-
- data class Response(
- @JsonProperty("access_token")
- val accessToken: String,
- @JsonProperty("token_type")
- val tokenType: String,
- val scope: String = "",
- @JsonProperty("expires_in")
- val expiresIn: Long
- )
-
- val result = client.post<Response> {
- url(Url("https://${auth.domain}/oauth/token"))
- contentType(ContentType.Application.Json)
- body = Request(audience, "client_credentials", auth.clientId, auth.clientSecret)
- }
-
- return BearerTokens(result.accessToken, "")
- }
-
- override fun close() = client.close()
-}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTrace.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTrace.kt
deleted file mode 100644
index adff6d97..00000000
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/ScenarioTrace.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) 2021 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.
- */
-
-package org.opendc.web.client.model
-
-/**
- * The trace details of a scenario.
- */
-public data class ScenarioTrace(val traceId: String, val loadSamplingFraction: Double)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/Main.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/Main.kt
index 94ef8f8e..561dcd59 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/Main.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/Main.kt
@@ -44,15 +44,15 @@ import org.opendc.simulator.compute.power.SimplePowerDriver
import org.opendc.simulator.core.runBlockingSimulation
import org.opendc.telemetry.compute.collectServiceMetrics
import org.opendc.telemetry.sdk.metrics.export.CoroutineMetricReader
-import org.opendc.web.client.ApiClient
-import org.opendc.web.client.AuthConfiguration
-import org.opendc.web.client.model.Scenario
+import org.opendc.web.client.auth.OpenIdAuthController
+import org.opendc.web.client.runner.OpenDCRunnerClient
+import org.opendc.web.proto.runner.Job
+import org.opendc.web.proto.runner.Scenario
import java.io.File
import java.net.URI
import java.time.Duration
import java.util.*
-import org.opendc.web.client.model.Portfolio as ClientPortfolio
-import org.opendc.web.client.model.Topology as ClientTopology
+import org.opendc.web.proto.runner.Topology as ClientTopology
private val logger = KotlinLogging.logger {}
@@ -134,18 +134,18 @@ class RunnerCli : CliktCommand(name = "runner") {
.default(60L * 3) // Experiment may run for a maximum of three minutes
/**
- * Converge a single scenario.
+ * Run a simulation job.
*/
- private suspend fun runScenario(portfolio: ClientPortfolio, scenario: Scenario, topology: Topology): List<WebComputeMetricExporter.Result> {
- val id = scenario.id
+ private suspend fun runJob(job: Job, topology: Topology): List<WebComputeMetricExporter.Result> {
+ val id = job.id
+ val scenario = job.scenario
logger.info { "Constructing performance interference model" }
val workloadLoader = ComputeWorkloadLoader(tracePath)
val interferenceModel = let {
- val path = tracePath.resolve(scenario.trace.traceId).resolve("performance-interference-model.json")
- val operational = scenario.operationalPhenomena
- val enabled = operational.performanceInterferenceEnabled
+ val path = tracePath.resolve(scenario.workload.trace.id).resolve("performance-interference-model.json")
+ val enabled = scenario.phenomena.interference
if (!enabled || !path.exists()) {
return@let null
@@ -154,8 +154,7 @@ class RunnerCli : CliktCommand(name = "runner") {
VmInterferenceModelReader().read(path.inputStream())
}
- val targets = portfolio.targets
- val results = (0 until targets.repeatsPerScenario).map { repeat ->
+ val results = (0 until scenario.portfolio.targets.repeats).map { repeat ->
logger.info { "Starting repeat $repeat" }
withTimeout(runTimeout * 1000) {
runRepeat(scenario, repeat, topology, workloadLoader, interferenceModel?.withSeed(repeat.toLong()))
@@ -168,7 +167,7 @@ class RunnerCli : CliktCommand(name = "runner") {
}
/**
- * Converge a single repeat.
+ * Run a single repeat.
*/
private suspend fun runRepeat(
scenario: Scenario,
@@ -181,17 +180,17 @@ class RunnerCli : CliktCommand(name = "runner") {
try {
runBlockingSimulation {
- val workloadName = scenario.trace.traceId
- val workloadFraction = scenario.trace.loadSamplingFraction
+ val workloadName = scenario.workload.trace.id
+ val workloadFraction = scenario.workload.samplingFraction
val seeder = Random(repeat.toLong())
- val operational = scenario.operationalPhenomena
- val computeScheduler = createComputeScheduler(operational.schedulerName, seeder)
+ val phenomena = scenario.phenomena
+ val computeScheduler = createComputeScheduler(scenario.schedulerName, seeder)
val workload = trace(workloadName).sampleByLoad(workloadFraction)
val failureModel =
- if (operational.failuresEnabled)
+ if (phenomena.failures)
grid5000(Duration.ofDays(7))
else
null
@@ -203,7 +202,7 @@ class RunnerCli : CliktCommand(name = "runner") {
telemetry,
computeScheduler,
failureModel,
- interferenceModel.takeIf { operational.performanceInterferenceEnabled }
+ interferenceModel
)
telemetry.registerMetricReader(CoroutineMetricReader(this, exporter, exportInterval = Duration.ofHours(1)))
@@ -241,20 +240,19 @@ class RunnerCli : CliktCommand(name = "runner") {
override fun run(): Unit = runBlocking(Dispatchers.Default) {
logger.info { "Starting OpenDC web runner" }
- val client = ApiClient(baseUrl = apiUrl, AuthConfiguration(authDomain, authClientId, authClientSecret), authAudience)
+ val client = OpenDCRunnerClient(baseUrl = apiUrl, OpenIdAuthController(authDomain, authClientId, authClientSecret, authAudience))
val manager = ScenarioManager(client)
logger.info { "Watching for queued scenarios" }
while (true) {
- val scenario = manager.findNext()
-
- if (scenario == null) {
+ val job = manager.findNext()
+ if (job == null) {
delay(POLL_INTERVAL)
continue
}
- val id = scenario.id
+ val id = job.id
logger.info { "Found queued scenario $id: attempting to claim" }
@@ -273,10 +271,8 @@ class RunnerCli : CliktCommand(name = "runner") {
}
try {
- val scenarioModel = client.getScenario(id)!!
- val portfolio = client.getPortfolio(scenarioModel.portfolioId)!!
- val environment = convert(client.getTopology(scenarioModel.topology.topologyId)!!)
- val results = runScenario(portfolio, scenarioModel, environment)
+ val environment = convert(job.scenario.topology)
+ val results = runJob(job, environment)
logger.info { "Writing results to database" }
@@ -306,7 +302,8 @@ class RunnerCli : CliktCommand(name = "runner") {
val machines = topology.rooms.asSequence()
.flatMap { room ->
room.tiles.flatMap { tile ->
- tile.rack?.machines?.map { machine -> tile.rack to machine } ?: emptyList()
+ val rack = tile.rack
+ rack?.machines?.map { machine -> rack to machine } ?: emptyList()
}
}
for ((rack, machine) in machines) {
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/ScenarioManager.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/ScenarioManager.kt
index 1ee835a6..7374f0c9 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/ScenarioManager.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/ScenarioManager.kt
@@ -22,64 +22,68 @@
package org.opendc.web.runner
-import org.opendc.web.client.ApiClient
-import org.opendc.web.client.model.Job
-import org.opendc.web.client.model.SimulationState
+import org.opendc.web.client.runner.OpenDCRunnerClient
+import org.opendc.web.proto.JobState
+import org.opendc.web.proto.runner.Job
/**
* Manages the queue of scenarios that need to be processed.
*/
-public class ScenarioManager(private val client: ApiClient) {
+class ScenarioManager(private val client: OpenDCRunnerClient) {
/**
* Find the next job that the simulator needs to process.
*/
- public suspend fun findNext(): Job? {
- return client.getJobs().firstOrNull()
+ fun findNext(): Job? {
+ return client.jobs.queryPending().firstOrNull()
}
/**
* Claim the simulation job with the specified id.
*/
- public suspend fun claim(id: String): Boolean {
- return client.updateJob(id, SimulationState.CLAIMED)
+ fun claim(id: Long): Boolean {
+ client.jobs.update(id, Job.Update(JobState.CLAIMED)) // TODO Handle conflict
+ return true
}
/**
* Update the heartbeat of the specified scenario.
*/
- public suspend fun heartbeat(id: String) {
- client.updateJob(id, SimulationState.RUNNING)
+ fun heartbeat(id: Long) {
+ client.jobs.update(id, Job.Update(JobState.RUNNING))
}
/**
* Mark the scenario as failed.
*/
- public suspend fun fail(id: String) {
- client.updateJob(id, SimulationState.FAILED)
+ fun fail(id: Long) {
+ client.jobs.update(id, Job.Update(JobState.FAILED))
}
/**
* Persist the specified results.
*/
- public suspend fun finish(id: String, results: List<WebComputeMetricExporter.Result>) {
- client.updateJob(
- id, SimulationState.FINISHED,
- mapOf(
- "total_requested_burst" to results.map { it.totalActiveTime + it.totalIdleTime },
- "total_granted_burst" to results.map { it.totalActiveTime },
- "total_overcommitted_burst" to results.map { it.totalStealTime },
- "total_interfered_burst" to results.map { it.totalLostTime },
- "mean_cpu_usage" to results.map { it.meanCpuUsage },
- "mean_cpu_demand" to results.map { it.meanCpuDemand },
- "mean_num_deployed_images" to results.map { it.meanNumDeployedImages },
- "max_num_deployed_images" to results.map { it.maxNumDeployedImages },
- "total_power_draw" to results.map { it.totalPowerDraw },
- "total_failure_slices" to results.map { it.totalFailureSlices },
- "total_failure_vm_slices" to results.map { it.totalFailureVmSlices },
- "total_vms_submitted" to results.map { it.totalVmsSubmitted },
- "total_vms_queued" to results.map { it.totalVmsQueued },
- "total_vms_finished" to results.map { it.totalVmsFinished },
- "total_vms_failed" to results.map { it.totalVmsFailed }
+ public fun finish(id: Long, results: List<WebComputeMetricExporter.Result>) {
+ client.jobs.update(
+ id,
+ Job.Update(
+ JobState.FINISHED,
+ mapOf(
+ "total_requested_burst" to results.map { it.totalActiveTime + it.totalIdleTime },
+ "total_granted_burst" to results.map { it.totalActiveTime },
+ "total_overcommitted_burst" to results.map { it.totalStealTime },
+ "total_interfered_burst" to results.map { it.totalLostTime },
+ "mean_cpu_usage" to results.map { it.meanCpuUsage },
+ "mean_cpu_demand" to results.map { it.meanCpuDemand },
+ "mean_num_deployed_images" to results.map { it.meanNumDeployedImages },
+ "max_num_deployed_images" to results.map { it.maxNumDeployedImages },
+ "total_power_draw" to results.map { it.totalPowerDraw },
+ "total_failure_slices" to results.map { it.totalFailureSlices },
+ "total_failure_vm_slices" to results.map { it.totalFailureVmSlices },
+ "total_vms_submitted" to results.map { it.totalVmsSubmitted },
+ "total_vms_queued" to results.map { it.totalVmsQueued },
+ "total_vms_finished" to results.map { it.totalVmsFinished },
+ "total_vms_failed" to results.map { it.totalVmsFailed }
+ )
)
)
}
diff --git a/opendc-web/opendc-web-runner/src/test/kotlin/org/opendc/web/client/ApiClientTest.kt b/opendc-web/opendc-web-runner/src/test/kotlin/org/opendc/web/client/ApiClientTest.kt
deleted file mode 100644
index 3a0730a6..00000000
--- a/opendc-web/opendc-web-runner/src/test/kotlin/org/opendc/web/client/ApiClientTest.kt
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (c) 2021 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.
- */
-
-package org.opendc.web.client
-
-import io.ktor.client.*
-import io.ktor.client.engine.mock.*
-import io.ktor.http.*
-import kotlinx.coroutines.runBlocking
-import org.junit.jupiter.api.Assertions.assertNotNull
-import org.junit.jupiter.api.Assertions.assertNull
-import org.junit.jupiter.api.Test
-import java.net.URI
-
-/**
- * Test suite for the [ApiClient] class.
- */
-class ApiClientTest {
- /**
- * The Ktor [HttpClient] instance.
- */
- private val ktor = HttpClient(MockEngine) {
- engine {
- addHandler { request ->
- when (request.url.fullPath) {
- "/oauth/token" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "access_token": "eyJz93a...k4laUWw",
- "token_type": "Bearer",
- "expires_in": 86400
- }
- """.trimIndent(),
- headers = responseHeaders
- )
- }
- "/portfolios/5fda5daa97dca438e7cb0a4c" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "data": {
- "_id": "string",
- "projectId": "string",
- "name": "string",
- "scenarioIds": [
- "string"
- ],
- "targets": {
- "enabledMetrics": [
- "string"
- ],
- "repeatsPerScenario": 0
- }
- }
- }
- """.trimIndent(),
- headers = responseHeaders
- )
- }
- "/portfolios/x" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "message": "Not Found"
- }
- """.trimIndent(),
- headers = responseHeaders, status = HttpStatusCode.NotFound
- )
- }
- "/scenarios/5fda5db297dca438e7cb0a4d" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "data": {
- "_id": "string",
- "portfolioId": "string",
- "name": "string",
- "trace": {
- "traceId": "string",
- "loadSamplingFraction": 0
- },
- "topology": {
- "topologyId": "string"
- },
- "operational": {
- "failuresEnabled": true,
- "performanceInterferenceEnabled": true,
- "schedulerName": "string"
- }
- }
- }
- """.trimIndent(),
- headers = responseHeaders
- )
- }
- "/scenarios/x" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "message": "Not Found"
- }
- """.trimIndent(),
- headers = responseHeaders, status = HttpStatusCode.NotFound
- )
- }
- "/topologies/5f9825a6cf6e4c24e380b86f" -> {
- val responseHeaders = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "data": {
- "_id": "string",
- "projectId": "string",
- "name": "string",
- "rooms": [
- {
- "_id": "string",
- "name": "string",
- "tiles": [
- {
- "_id": "string",
- "positionX": 0,
- "positionY": 0,
- "rack": {
- "_id": "string",
- "name": "string",
- "capacity": 0,
- "powerCapacityW": 0,
- "machines": [
- {
- "_id": "string",
- "position": 0,
- "cpus": [
- {
- "_id": "string",
- "name": "string",
- "clockRateMhz": 0,
- "numberOfCores": 0
- }
- ],
- "gpus": [
- {
- "_id": "string",
- "name": "string",
- "clockRateMhz": 0,
- "numberOfCores": 0
- }
- ],
- "memories": [
- {
- "_id": "string",
- "name": "string",
- "speedMbPerS": 0,
- "sizeMb": 0
- }
- ],
- "storages": [
- {
- "_id": "string",
- "name": "string",
- "speedMbPerS": 0,
- "sizeMb": 0
- }
- ]
- }
- ]
- }
- }
- ]
- }
- ]
- }
- }
- """.trimIndent(),
- headers = responseHeaders
- )
- }
- "/topologies/x" -> {
- val responseHeaders =
- headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
- respond(
- """
- {
- "message": "Not Found"
- }
- """.trimIndent(),
- headers = responseHeaders, status = HttpStatusCode.NotFound
- )
- }
- else -> error("Unhandled ${request.url}")
- }
- }
- }
- }
-
- private val auth = AuthConfiguration("auth.opendc.org", "a", "b")
-
- @Test
- fun testPortfolioExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val portfolio = client.getPortfolio("5fda5daa97dca438e7cb0a4c")
- assertNotNull(portfolio)
- }
-
- @Test
- fun testPortfolioDoesNotExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val portfolio = client.getPortfolio("x")
- assertNull(portfolio)
- }
-
- @Test
- fun testScenarioExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val scenario = client.getScenario("5fda5db297dca438e7cb0a4d")
- assertNotNull(scenario)
- }
-
- @Test
- fun testScenarioDoesNotExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val scenario = client.getScenario("x")
- assertNull(scenario)
- }
-
- @Test
- fun testTopologyExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val topology = client.getTopology("5f9825a6cf6e4c24e380b86f")
- assertNotNull(topology)
- }
-
- @Test
- fun testTopologyDoesNotExists(): Unit = runBlocking {
- val client = ApiClient(URI("http://localhost:8081"), auth, client = ktor)
- val topology = client.getTopology("x")
- assertNull(topology)
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js
index 680d49ce..1a9877d0 100644
--- a/opendc-web/opendc-web-ui/src/api/index.js
+++ b/opendc-web/opendc-web-ui/src/api/index.js
@@ -47,5 +47,5 @@ export async function request(auth, path, method = 'GET', body) {
throw response.message
}
- return json.data
+ return json
}
diff --git a/opendc-web/opendc-web-ui/src/api/portfolios.js b/opendc-web/opendc-web-ui/src/api/portfolios.js
index 82ac0ced..d818876f 100644
--- a/opendc-web/opendc-web-ui/src/api/portfolios.js
+++ b/opendc-web/opendc-web-ui/src/api/portfolios.js
@@ -22,22 +22,18 @@
import { request } from './index'
-export function fetchPortfolio(auth, portfolioId) {
- return request(auth, `portfolios/${portfolioId}`)
+export function fetchPortfolio(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/portfolios/${number}`)
}
-export function fetchPortfoliosOfProject(auth, projectId) {
+export function fetchPortfolios(auth, projectId) {
return request(auth, `projects/${projectId}/portfolios`)
}
-export function addPortfolio(auth, portfolio) {
- return request(auth, `projects/${portfolio.projectId}/portfolios`, 'POST', { portfolio })
+export function addPortfolio(auth, projectId, portfolio) {
+ return request(auth, `projects/${projectId}/portfolios`, 'POST', portfolio)
}
-export function updatePortfolio(auth, portfolioId, portfolio) {
- return request(auth, `portfolios/${portfolioId}`, 'PUT', { portfolio })
-}
-
-export function deletePortfolio(auth, portfolioId) {
- return request(auth, `portfolios/${portfolioId}`, 'DELETE')
+export function deletePortfolio(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/portfolios/${number}`, 'DELETE')
}
diff --git a/opendc-web/opendc-web-ui/src/api/projects.js b/opendc-web/opendc-web-ui/src/api/projects.js
index 4123b371..e7e095da 100644
--- a/opendc-web/opendc-web-ui/src/api/projects.js
+++ b/opendc-web/opendc-web-ui/src/api/projects.js
@@ -31,11 +31,7 @@ export function fetchProject(auth, projectId) {
}
export function addProject(auth, project) {
- return request(auth, 'projects/', 'POST', { project })
-}
-
-export function updateProject(auth, project) {
- return request(auth, `projects/${project._id}`, 'PUT', { project })
+ return request(auth, 'projects/', 'POST', project)
}
export function deleteProject(auth, projectId) {
diff --git a/opendc-web/opendc-web-ui/src/api/scenarios.js b/opendc-web/opendc-web-ui/src/api/scenarios.js
index 88516caa..7eeb8f28 100644
--- a/opendc-web/opendc-web-ui/src/api/scenarios.js
+++ b/opendc-web/opendc-web-ui/src/api/scenarios.js
@@ -22,22 +22,18 @@
import { request } from './index'
-export function fetchScenario(auth, scenarioId) {
- return request(auth, `scenarios/${scenarioId}`)
+export function fetchScenario(auth, projectId, scenarioId) {
+ return request(auth, `projects/${projectId}/scenarios/${scenarioId}`)
}
-export function fetchScenariosOfPortfolio(auth, portfolioId) {
- return request(auth, `portfolios/${portfolioId}/scenarios`)
+export function fetchScenariosOfPortfolio(auth, projectId, portfolioId) {
+ return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`)
}
-export function addScenario(auth, scenario) {
- return request(auth, `portfolios/${scenario.portfolioId}/scenarios`, 'POST', { scenario })
+export function addScenario(auth, projectId, portfolioId, scenario) {
+ return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`, 'POST', scenario)
}
-export function updateScenario(auth, scenarioId, scenario) {
- return request(auth, `scenarios/${scenarioId}`, 'PUT', { scenario })
-}
-
-export function deleteScenario(auth, scenarioId) {
- return request(auth, `scenarios/${scenarioId}`, 'DELETE')
+export function deleteScenario(auth, projectId, scenarioId) {
+ return request(auth, `projects/${projectId}/scenarios/${scenarioId}`, 'DELETE')
}
diff --git a/opendc-web/opendc-web-ui/src/api/topologies.js b/opendc-web/opendc-web-ui/src/api/topologies.js
index bd4e3bc4..0509c6d0 100644
--- a/opendc-web/opendc-web-ui/src/api/topologies.js
+++ b/opendc-web/opendc-web-ui/src/api/topologies.js
@@ -22,24 +22,23 @@
import { request } from './index'
-export function fetchTopology(auth, topologyId) {
- return request(auth, `topologies/${topologyId}`)
+export function fetchTopology(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/topologies/${number}`)
}
-export function fetchTopologiesOfProject(auth, projectId) {
+export function fetchTopologies(auth, projectId) {
return request(auth, `projects/${projectId}/topologies`)
}
-export function addTopology(auth, topology) {
- return request(auth, `projects/${topology.projectId}/topologies`, 'POST', { topology })
+export function addTopology(auth, projectId, topology) {
+ return request(auth, `projects/${projectId}/topologies`, 'POST', topology)
}
export function updateTopology(auth, topology) {
- // eslint-disable-next-line no-unused-vars
- const { _id, ...data } = topology
- return request(auth, `topologies/${topology._id}`, 'PUT', { topology: data })
+ const { project, number, rooms } = topology
+ return request(auth, `projects/${project.id}/topologies/${number}`, 'PUT', { rooms })
}
-export function deleteTopology(auth, topologyId) {
- return request(auth, `topologies/${topologyId}`, 'DELETE')
+export function deleteTopology(auth, projectId, number) {
+ return request(auth, `projects/${projectId}/topologies/${number}`, 'DELETE')
}
diff --git a/opendc-web/opendc-web-ui/src/components/AppNavigation.js b/opendc-web/opendc-web-ui/src/components/AppNavigation.js
index 178c3ec0..77c683a2 100644
--- a/opendc-web/opendc-web-ui/src/components/AppNavigation.js
+++ b/opendc-web/opendc-web-ui/src/components/AppNavigation.js
@@ -23,15 +23,9 @@
import { Nav, NavItem, NavList } from '@patternfly/react-core'
import { useRouter } from 'next/router'
import NavItemLink from './util/NavItemLink'
-import { useProject } from '../data/project'
export function AppNavigation() {
- const { pathname, query } = useRouter()
- const { project: projectId } = query
- const { data: project } = useProject(projectId)
-
- const nextTopologyId = project?.topologyIds?.[0]
- const nextPortfolioId = project?.portfolioIds?.[0]
+ const { pathname } = useRouter()
return (
<Nav variant="horizontal">
@@ -45,28 +39,6 @@ export function AppNavigation() {
>
Projects
</NavItem>
- {pathname.startsWith('/projects/[project]') && (
- <>
- <NavItem
- id="topologies"
- to={nextTopologyId ? `/projects/${projectId}/topologies/${nextTopologyId}` : '/projects'}
- itemId={1}
- component={NavItemLink}
- isActive={pathname === '/projects/[project]/topologies/[topology]'}
- >
- Topologies
- </NavItem>
- <NavItem
- id="portfolios"
- to={nextPortfolioId ? `/projects/${projectId}/portfolios/${nextPortfolioId}` : '/projects'}
- itemId={2}
- component={NavItemLink}
- isActive={pathname === '/projects/[project]/portfolios/[portfolio]'}
- >
- Portfolios
- </NavItem>
- </>
- )}
</NavList>
</Nav>
)
diff --git a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js
index 3712cfa0..a99b60c0 100644
--- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js
+++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js
@@ -22,13 +22,11 @@
import PropTypes from 'prop-types'
import { ContextSelector as PFContextSelector, ContextSelectorItem } from '@patternfly/react-core'
-import { useMemo, useState, useReducer } from 'react'
+import { useMemo, useState } from 'react'
import { contextSelector } from './ContextSelector.module.scss'
-function ContextSelector({ activeItem, items, onSelect, label }) {
- const [isOpen, toggle] = useReducer((isOpen) => !isOpen, false)
+function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label }) {
const [searchValue, setSearchValue] = useState('')
-
const filteredItems = useMemo(
() => items.filter(({ name }) => name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1) || items,
[items, searchValue]
@@ -36,23 +34,22 @@ function ContextSelector({ activeItem, items, onSelect, label }) {
return (
<PFContextSelector
- menuAppendTo={global.document?.body}
className={contextSelector}
toggleText={activeItem ? `${label}: ${activeItem.name}` : label}
onSearchInputChange={(value) => setSearchValue(value)}
searchInputValue={searchValue}
isOpen={isOpen}
- onToggle={toggle}
+ onToggle={(_, isOpen) => onToggle(isOpen)}
onSelect={(event) => {
- const targetId = event.target.value
- const target = items.find((item) => item._id === targetId)
+ const targetId = +event.target.value
+ const target = items.find((item) => item.id === targetId)
- toggle()
onSelect(target)
+ onToggle(!isOpen)
}}
>
{filteredItems.map((item) => (
- <ContextSelectorItem key={item._id} value={item._id}>
+ <ContextSelectorItem key={item.id} value={item.id}>
{item.name}
</ContextSelectorItem>
))}
@@ -61,7 +58,7 @@ function ContextSelector({ activeItem, items, onSelect, label }) {
}
const Item = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.any.isRequired,
name: PropTypes.string.isRequired,
})
@@ -69,6 +66,8 @@ ContextSelector.propTypes = {
activeItem: Item,
items: PropTypes.arrayOf(Item).isRequired,
onSelect: PropTypes.func.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool,
label: PropTypes.string,
}
diff --git a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss
index fefba41f..0aa63ee6 100644
--- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss
+++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss
@@ -21,7 +21,6 @@
*/
.contextSelector {
- width: auto;
margin-right: 20px;
--pf-c-context-selector__toggle--PaddingTop: var(--pf-global--spacer--sm);
diff --git a/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js b/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js
index 694681ac..c4f2d50e 100644
--- a/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js
+++ b/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js
@@ -21,27 +21,31 @@
*/
import { useRouter } from 'next/router'
-import { useMemo } from 'react'
-import { useProjectPortfolios } from '../../data/project'
+import { useState } from 'react'
+import { usePortfolios } from '../../data/project'
+import { Portfolio } from '../../shapes'
import ContextSelector from './ContextSelector'
-function PortfolioSelector() {
+function PortfolioSelector({ activePortfolio }) {
const router = useRouter()
- const { project, portfolio: activePortfolioId } = router.query
- const { data: portfolios = [] } = useProjectPortfolios(project)
- const activePortfolio = useMemo(() => portfolios.find((portfolio) => portfolio._id === activePortfolioId), [
- activePortfolioId,
- portfolios,
- ])
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: portfolios = [] } = usePortfolios(activePortfolio?.project?.id, { enabled: isOpen })
return (
<ContextSelector
label="Portfolio"
activeItem={activePortfolio}
items={portfolios}
- onSelect={(portfolio) => router.push(`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`)}
+ onSelect={(portfolio) => router.push(`/projects/${portfolio.project.id}/portfolios/${portfolio.number}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
/>
)
}
+PortfolioSelector.propTypes = {
+ activePortfolio: Portfolio,
+}
+
export default PortfolioSelector
diff --git a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js
index 753632ab..7721e04c 100644
--- a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js
+++ b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js
@@ -20,29 +20,32 @@
* SOFTWARE.
*/
-import PropTypes from 'prop-types'
import { useRouter } from 'next/router'
-import { useMemo } from 'react'
+import { useState } from 'react'
import { useProjects } from '../../data/project'
+import { Project } from '../../shapes'
import ContextSelector from './ContextSelector'
-function ProjectSelector({ projectId }) {
+function ProjectSelector({ activeProject }) {
const router = useRouter()
- const { data: projects = [] } = useProjects()
- const activeProject = useMemo(() => projects.find((project) => project._id === projectId), [projectId, projects])
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: projects = [] } = useProjects({ enabled: isOpen })
return (
<ContextSelector
label="Project"
activeItem={activeProject}
items={projects}
- onSelect={(project) => router.push(`/projects/${project._id}`)}
+ onSelect={(project) => router.push(`/projects/${project.id}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
/>
)
}
ProjectSelector.propTypes = {
- projectId: PropTypes.string,
+ activeProject: Project,
}
export default ProjectSelector
diff --git a/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js b/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js
index d5e51c6c..9cae4cbf 100644
--- a/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js
+++ b/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js
@@ -20,33 +20,32 @@
* SOFTWARE.
*/
-import PropTypes from 'prop-types'
import { useRouter } from 'next/router'
-import { useMemo } from 'react'
-import { useProjectTopologies } from '../../data/topology'
+import { useState } from 'react'
+import { useTopologies } from '../../data/topology'
+import { Topology } from '../../shapes'
import ContextSelector from './ContextSelector'
-function TopologySelector({ projectId, topologyId }) {
+function TopologySelector({ activeTopology }) {
const router = useRouter()
- const { data: topologies = [] } = useProjectTopologies(projectId)
- const activeTopology = useMemo(() => topologies.find((topology) => topology._id === topologyId), [
- topologyId,
- topologies,
- ])
+
+ const [isOpen, setOpen] = useState(false)
+ const { data: topologies = [] } = useTopologies(activeTopology?.project?.id, { enabled: isOpen })
return (
<ContextSelector
label="Topology"
activeItem={activeTopology}
items={topologies}
- onSelect={(topology) => router.push(`/projects/${topology.projectId}/topologies/${topology._id}`)}
+ onSelect={(topology) => router.push(`/projects/${topology.project.id}/topologies/${topology.number}`)}
+ onToggle={setOpen}
+ isOpen={isOpen}
/>
)
}
TopologySelector.propTypes = {
- projectId: PropTypes.string,
- topologyId: PropTypes.string,
+ activeTopology: Topology,
}
export default TopologySelector
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js
index 856282a7..fd9a72d2 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js
@@ -24,21 +24,15 @@ import PropTypes from 'prop-types'
import { PlusIcon } from '@patternfly/react-icons'
import { Button } from '@patternfly/react-core'
import { useState } from 'react'
-import { useMutation } from 'react-query'
+import { useNewScenario } from '../../data/project'
import NewScenarioModal from './NewScenarioModal'
-function NewScenario({ portfolioId }) {
+function NewScenario({ projectId, portfolioId }) {
const [isVisible, setVisible] = useState(false)
- const { mutate: addScenario } = useMutation('addScenario')
+ const { mutate: addScenario } = useNewScenario()
- const onSubmit = (name, portfolioId, trace, topology, operational) => {
- addScenario({
- portfolioId,
- name,
- trace,
- topology,
- operational,
- })
+ const onSubmit = (projectId, portfolioNumber, data) => {
+ addScenario({ projectId, portfolioNumber, data })
setVisible(false)
}
@@ -48,6 +42,7 @@ function NewScenario({ portfolioId }) {
New Scenario
</Button>
<NewScenarioModal
+ projectId={projectId}
portfolioId={portfolioId}
isOpen={isVisible}
onSubmit={onSubmit}
@@ -58,7 +53,8 @@ function NewScenario({ portfolioId }) {
}
NewScenario.propTypes = {
- portfolioId: PropTypes.string,
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
}
export default NewScenario
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js
index 7f620c8c..ed35c163 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js
@@ -12,14 +12,14 @@ import {
TextInput,
} from '@patternfly/react-core'
import { useSchedulers, useTraces } from '../../data/experiments'
-import { useProjectTopologies } from '../../data/topology'
+import { useTopologies } from '../../data/topology'
import { usePortfolio } from '../../data/project'
-const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => {
- const { data: portfolio } = usePortfolio(portfolioId)
- const { data: topologies = [] } = useProjectTopologies(portfolio?.projectId)
- const { data: traces = [] } = useTraces()
- const { data: schedulers = [] } = useSchedulers()
+function NewScenarioModal({ projectId, portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) {
+ const { data: portfolio } = usePortfolio(projectId, portfolioId)
+ const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen })
+ const { data: traces = [] } = useTraces({ enabled: isOpen })
+ const { data: schedulers = [] } = useSchedulers({ enabled: isOpen })
// eslint-disable-next-line no-unused-vars
const [isSubmitted, setSubmitted] = useState(false)
@@ -51,22 +51,19 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
const name = nameInput.current.value
- onSubmitUpstream(
+ onSubmitUpstream(portfolio.project.id, portfolio.number, {
name,
- portfolio._id,
- {
- traceId: trace || traces[0]._id,
- loadSamplingFraction: traceLoad / 100,
+ workload: {
+ trace: trace || traces[0].id,
+ samplingFraction: traceLoad / 100,
},
- {
- topologyId: topology || topologies[0]._id,
+ topology: topology || topologies[0].number,
+ phenomena: {
+ failures: failuresEnabled,
+ interference: opPhenEnabled,
},
- {
- failuresEnabled,
- performanceInterferenceEnabled: opPhenEnabled,
- schedulerName: scheduler || schedulers[0].name,
- }
- )
+ schedulerName: scheduler || schedulers[0],
+ })
resetState()
return true
@@ -84,8 +81,8 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
id="name"
name="name"
type="text"
- isDisabled={portfolio?.scenarioIds?.length === 0}
- defaultValue={portfolio?.scenarioIds?.length === 0 ? 'Base scenario' : ''}
+ isDisabled={portfolio?.scenarios?.length === 0}
+ defaultValue={portfolio?.scenarios?.length === 0 ? 'Base scenario' : ''}
ref={nameInput}
/>
</FormGroup>
@@ -93,7 +90,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
<FormGroup label="Trace" fieldId="trace" isRequired>
<FormSelect id="trace" name="trace" value={trace} onChange={setTrace}>
{traces.map((trace) => (
- <FormSelectOption value={trace._id} key={trace._id} label={trace.name} />
+ <FormSelectOption value={trace.id} key={trace.id} label={trace.name} />
))}
</FormSelect>
</FormGroup>
@@ -115,7 +112,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
<FormGroup label="Topology" fieldId="topology" isRequired>
<FormSelect id="topology" name="topology" value={topology} onChange={setTopology}>
{topologies.map((topology) => (
- <FormSelectOption value={topology._id} key={topology._id} label={topology.name} />
+ <FormSelectOption value={topology.number} key={topology.number} label={topology.name} />
))}
</FormSelect>
</FormGroup>
@@ -123,7 +120,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
<FormGroup label="Scheduler" fieldId="scheduler" isRequired>
<FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}>
{schedulers.map((scheduler) => (
- <FormSelectOption value={scheduler.name} key={scheduler.name} label={scheduler.name} />
+ <FormSelectOption value={scheduler} key={scheduler} label={scheduler} />
))}
</FormSelect>
</FormGroup>
@@ -150,7 +147,8 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC
}
NewScenarioModal.propTypes = {
- portfolioId: PropTypes.string,
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js
index 580b0a29..e561b655 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js
@@ -43,8 +43,8 @@ import { METRIC_NAMES } from '../../util/available-metrics'
import NewScenario from './NewScenario'
import ScenarioTable from './ScenarioTable'
-function PortfolioOverview({ portfolioId }) {
- const { data: portfolio } = usePortfolio(portfolioId)
+function PortfolioOverview({ projectId, portfolioId }) {
+ const { status, data: portfolio } = usePortfolio(projectId, portfolioId)
return (
<Grid hasGutter>
@@ -62,16 +62,16 @@ function PortfolioOverview({ portfolioId }) {
<DescriptionListGroup>
<DescriptionListTerm>Scenarios</DescriptionListTerm>
<DescriptionListDescription>
- {portfolio?.scenarioIds.length ?? <Skeleton screenreaderText="Loading portfolio" />}
+ {portfolio?.scenarios?.length ?? <Skeleton screenreaderText="Loading portfolio" />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Metrics</DescriptionListTerm>
<DescriptionListDescription>
- {portfolio?.targets?.enabledMetrics ? (
- portfolio.targets.enabledMetrics.length > 0 ? (
+ {portfolio ? (
+ portfolio.targets.metrics.length > 0 ? (
<ChipGroup>
- {portfolio.targets.enabledMetrics.map((metric) => (
+ {portfolio.targets.metrics.map((metric) => (
<Chip isReadOnly key={metric}>
{METRIC_NAMES[metric]}
</Chip>
@@ -88,9 +88,7 @@ function PortfolioOverview({ portfolioId }) {
<DescriptionListGroup>
<DescriptionListTerm>Repeats per Scenario</DescriptionListTerm>
<DescriptionListDescription>
- {portfolio?.targets?.repeatsPerScenario ?? (
- <Skeleton screenreaderText="Loading portfolio" />
- )}
+ {portfolio?.targets?.repeats ?? <Skeleton screenreaderText="Loading portfolio" />}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
@@ -101,12 +99,12 @@ function PortfolioOverview({ portfolioId }) {
<Card>
<CardHeader>
<CardActions>
- <NewScenario portfolioId={portfolioId} />
+ <NewScenario projectId={projectId} portfolioId={portfolioId} />
</CardActions>
<CardTitle>Scenarios</CardTitle>
</CardHeader>
<CardBody>
- <ScenarioTable portfolioId={portfolioId} />
+ <ScenarioTable portfolio={portfolio} status={status} />
</CardBody>
</Card>
</GridItem>
@@ -115,7 +113,8 @@ function PortfolioOverview({ portfolioId }) {
}
PortfolioOverview.propTypes = {
- portfolioId: PropTypes.string,
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
}
export default PortfolioOverview
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
index 00023d9e..f63f0c7f 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
@@ -42,12 +42,14 @@ import {
Title,
} from '@patternfly/react-core'
import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons'
-import { usePortfolioScenarios } from '../../data/project'
+import { usePortfolio } from '../../data/project'
import PortfolioResultInfo from './PortfolioResultInfo'
import NewScenario from './NewScenario'
-const PortfolioResults = ({ portfolioId }) => {
- const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId)
+const PortfolioResults = ({ projectId, portfolioId }) => {
+ const { status, data: scenarios = [] } = usePortfolio(projectId, portfolioId, {
+ select: (portfolio) => portfolio.scenarios,
+ })
if (status === 'loading') {
return (
@@ -86,7 +88,7 @@ const PortfolioResults = ({ portfolioId }) => {
No results are currently available for this portfolio. Run a scenario to obtain simulation
results.
</EmptyStateBody>
- <NewScenario portfolioId={portfolioId} />
+ <NewScenario projectId={projectId} portfolioId={portfolioId} />
</EmptyState>
</Bullseye>
)
@@ -96,11 +98,11 @@ const PortfolioResults = ({ portfolioId }) => {
AVAILABLE_METRICS.forEach((metric) => {
dataPerMetric[metric] = scenarios
- .filter((scenario) => scenario.results)
+ .filter((scenario) => scenario.job?.results)
.map((scenario) => ({
name: scenario.name,
- value: mean(scenario.results[metric]),
- errorX: std(scenario.results[metric]),
+ value: mean(scenario.job.results[metric]),
+ errorX: std(scenario.job.results[metric]),
}))
})
@@ -150,7 +152,8 @@ const PortfolioResults = ({ portfolioId }) => {
}
PortfolioResults.propTypes = {
- portfolioId: PropTypes.string,
+ projectId: PropTypes.number,
+ portfolioId: PropTypes.number,
}
export default PortfolioResults
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js
index 66691580..99d83f64 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js
@@ -20,13 +20,13 @@
* SOFTWARE.
*/
-import PropTypes from 'prop-types'
import { ClockIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons'
+import { JobState } from '../../shapes'
function ScenarioState({ state }) {
switch (state) {
+ case 'PENDING':
case 'CLAIMED':
- case 'QUEUED':
return (
<span>
<ClockIcon color="blue" /> Queued
@@ -56,7 +56,7 @@ function ScenarioState({ state }) {
}
ScenarioState.propTypes = {
- state: PropTypes.oneOf(['QUEUED', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED']),
+ state: JobState.isRequired,
}
export default ScenarioState
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
index 9966e3ba..68647957 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
@@ -20,44 +20,38 @@
* SOFTWARE.
*/
-import PropTypes from 'prop-types'
import Link from 'next/link'
import { Table, TableBody, TableHeader } from '@patternfly/react-table'
import React from 'react'
+import { Portfolio, Status } from '../../shapes'
import TableEmptyState from '../util/TableEmptyState'
import ScenarioState from './ScenarioState'
-import { usePortfolio, usePortfolioScenarios } from '../../data/project'
-import { useProjectTopologies } from '../../data/topology'
-import { useMutation } from 'react-query'
+import { useDeleteScenario } from '../../data/project'
-const ScenarioTable = ({ portfolioId }) => {
- const { data: portfolio } = usePortfolio(portfolioId)
- const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId)
- const { data: topologies } = useProjectTopologies(portfolio?.projectId, {
- select: (topologies) => new Map(topologies.map((topology) => [topology._id, topology])),
- })
-
- const { mutate: deleteScenario } = useMutation('deleteScenario')
+function ScenarioTable({ portfolio, status }) {
+ const { mutate: deleteScenario } = useDeleteScenario()
+ const projectId = portfolio?.project?.id
+ const scenarios = portfolio?.scenarios ?? []
const columns = ['Name', 'Topology', 'Trace', 'State']
const rows =
scenarios.length > 0
? scenarios.map((scenario) => {
- const topology = topologies?.get(scenario.topology.topologyId)
+ const topology = scenario.topology
return [
scenario.name,
{
title: topology ? (
- <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}>
+ <Link href={`/projects/${projectId}/topologies/${topology.number}`}>
<a>{topology.name}</a>
</Link>
) : (
'Unknown Topology'
),
},
- scenario.trace.traceId,
- { title: <ScenarioState state={scenario.simulation.state} /> },
+ `${scenario.workload.trace.name} (${scenario.workload.samplingFraction * 100}%)`,
+ { title: <ScenarioState state={scenario.job.state} /> },
]
})
: [
@@ -82,7 +76,7 @@ const ScenarioTable = ({ portfolioId }) => {
const actionResolver = (_, { rowIndex }) => [
{
title: 'Delete Scenario',
- onClick: (_, rowId) => deleteScenario(scenarios[rowId]._id),
+ onClick: (_, rowId) => deleteScenario({ projectId: projectId, number: scenarios[rowId].number }),
isDisabled: rowIndex === 0,
},
]
@@ -102,7 +96,8 @@ const ScenarioTable = ({ portfolioId }) => {
}
ScenarioTable.propTypes = {
- portfolioId: PropTypes.string,
+ portfolio: Portfolio,
+ status: Status.isRequired,
}
export default ScenarioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
index 87ea059d..aebcc3c9 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
@@ -24,12 +24,12 @@ import PropTypes from 'prop-types'
import { PlusIcon } from '@patternfly/react-icons'
import { Button } from '@patternfly/react-core'
import { useState } from 'react'
-import { useMutation } from 'react-query'
+import { useNewPortfolio } from '../../data/project'
import NewPortfolioModal from './NewPortfolioModal'
function NewPortfolio({ projectId }) {
const [isVisible, setVisible] = useState(false)
- const { mutate: addPortfolio } = useMutation('addPortfolio')
+ const { mutate: addPortfolio } = useNewPortfolio()
const onSubmit = (name, targets) => {
addPortfolio({ projectId, name, targets })
@@ -47,7 +47,7 @@ function NewPortfolio({ projectId }) {
}
NewPortfolio.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
}
export default NewPortfolio
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js
index 4276d7d4..ba4bc819 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js
@@ -67,7 +67,7 @@ const NewPortfolioModal = ({ isOpen, onSubmit: onSubmitUpstream, onCancel: onUps
setErrors({ name: true })
return false
} else {
- onSubmitUpstream(name, { enabledMetrics: selectedMetrics, repeatsPerScenario: repeats })
+ onSubmitUpstream(name, { metrics: selectedMetrics, repeats })
}
clearState()
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
index 984264dc..bfa7c01a 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import { Button } from '@patternfly/react-core'
-import { useMutation } from 'react-query'
import { PlusIcon } from '@patternfly/react-icons'
+import { useNewProject } from '../../data/project'
import { buttonContainer } from './NewProject.module.scss'
import TextInputModal from '../util/modals/TextInputModal'
@@ -10,7 +10,7 @@ import TextInputModal from '../util/modals/TextInputModal'
*/
const NewProject = () => {
const [isVisible, setVisible] = useState(false)
- const { mutate: addProject } = useMutation('addProject')
+ const { mutate: addProject } = useNewProject()
const onSubmit = (name) => {
if (name) {
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
index 77c57d26..4c569c56 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
@@ -21,25 +21,17 @@
*/
import PropTypes from 'prop-types'
-import produce from 'immer'
import { PlusIcon } from '@patternfly/react-icons'
import { Button } from '@patternfly/react-core'
import { useState } from 'react'
-import { useMutation } from "react-query";
-import { useProjectTopologies } from "../../data/topology";
+import { useNewTopology } from '../../data/topology'
import NewTopologyModal from './NewTopologyModal'
function NewTopology({ projectId }) {
const [isVisible, setVisible] = useState(false)
- const { data: topologies = [] } = useProjectTopologies(projectId)
- const { mutate: addTopology } = useMutation('addTopology')
+ const { mutate: addTopology } = useNewTopology()
- const onSubmit = (name, duplicateId) => {
- const candidate = topologies.find((topology) => topology._id === duplicateId) || { projectId, rooms: [] }
- const topology = produce(candidate, (draft) => {
- delete draft._id
- draft.name = name
- })
+ const onSubmit = (topology) => {
addTopology(topology)
setVisible(false)
}
@@ -59,7 +51,7 @@ function NewTopology({ projectId }) {
}
NewTopology.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
}
export default NewTopology
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js
index a495f73e..be4256e3 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js
@@ -20,10 +20,11 @@
* SOFTWARE.
*/
+import produce from 'immer'
import PropTypes from 'prop-types'
import React, { useRef, useState } from 'react'
import { Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'
-import { useProjectTopologies } from '../../data/topology'
+import { useTopologies } from '../../data/topology'
import Modal from '../util/modals/Modal'
const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => {
@@ -32,10 +33,12 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan
const [originTopology, setOriginTopology] = useState(-1)
const [errors, setErrors] = useState({})
- const { data: topologies = [] } = useProjectTopologies(projectId)
+ const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen })
const clearState = () => {
- nameInput.current.value = ''
+ if (nameInput.current) {
+ nameInput.current.value = ''
+ }
setSubmitted(false)
setOriginTopology(-1)
setErrors({})
@@ -53,10 +56,13 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan
if (!name) {
setErrors({ name: true })
return false
- } else if (originTopology === -1) {
- onSubmitUpstream(name)
} else {
- onSubmitUpstream(name, originTopology)
+ const candidate = topologies.find((topology) => topology.id === originTopology) || { projectId, rooms: [] }
+ const topology = produce(candidate, (draft) => {
+ delete draft.id
+ draft.name = name
+ })
+ onSubmitUpstream(topology)
}
clearState()
@@ -84,7 +90,7 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan
<FormSelect id="origin" name="origin" value={originTopology} onChange={setOriginTopology}>
<FormSelectOption value={-1} key={-1} label="None - start from scratch" />
{topologies.map((topology) => (
- <FormSelectOption value={topology._id} key={topology._id} label={topology.name} />
+ <FormSelectOption value={topology.id} key={topology.id} label={topology.name} />
))}
</FormSelect>
</FormGroup>
@@ -94,7 +100,7 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan
}
NewTopologyModal.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
diff --git a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
index 45e399ed..aa679843 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
@@ -25,12 +25,11 @@ import Link from 'next/link'
import { Table, TableBody, TableHeader } from '@patternfly/react-table'
import React from 'react'
import TableEmptyState from '../util/TableEmptyState'
-import { useProjectPortfolios } from '../../data/project'
-import { useMutation } from 'react-query'
+import { usePortfolios, useDeletePortfolio } from '../../data/project'
const PortfolioTable = ({ projectId }) => {
- const { status, data: portfolios = [] } = useProjectPortfolios(projectId)
- const { mutate: deletePortfolio } = useMutation('deletePortfolio')
+ const { status, data: portfolios = [] } = usePortfolios(projectId)
+ const { mutate: deletePortfolio } = useDeletePortfolio()
const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats']
const rows =
@@ -38,20 +37,12 @@ const PortfolioTable = ({ projectId }) => {
? portfolios.map((portfolio) => [
{
title: (
- <Link href={`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`}>
- {portfolio.name}
- </Link>
+ <Link href={`/projects/${projectId}/portfolios/${portfolio.number}`}>{portfolio.name}</Link>
),
},
-
- portfolio.scenarioIds.length === 1 ? '1 scenario' : `${portfolio.scenarioIds.length} scenarios`,
-
- portfolio.targets.enabledMetrics.length === 1
- ? '1 metric'
- : `${portfolio.targets.enabledMetrics.length} metrics`,
- portfolio.targets.repeatsPerScenario === 1
- ? '1 repeat'
- : `${portfolio.targets.repeatsPerScenario} repeats`,
+ portfolio.scenarios.length === 1 ? '1 scenario' : `${portfolio.scenarios.length} scenarios`,
+ portfolio.targets.metrics.length === 1 ? '1 metric' : `${portfolio.targets.metrics.length} metrics`,
+ portfolio.targets.repeats === 1 ? '1 repeat' : `${portfolio.targets.repeats} repeats`,
])
: [
{
@@ -77,7 +68,7 @@ const PortfolioTable = ({ projectId }) => {
? [
{
title: 'Delete Portfolio',
- onClick: (_, rowId) => deletePortfolio(portfolios[rowId]._id),
+ onClick: (_, rowId) => deletePortfolio({ projectId, number: portfolios[rowId].number }),
},
]
: []
@@ -91,7 +82,7 @@ const PortfolioTable = ({ projectId }) => {
}
PortfolioTable.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
}
export default PortfolioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js
index 65b8f5a0..3e1656f6 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js
@@ -92,7 +92,7 @@ function ProjectOverview({ projectId }) {
}
ProjectOverview.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
}
export default ProjectOverview
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
index a7290259..6921578c 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
@@ -5,26 +5,23 @@ import { Project, Status } from '../../shapes'
import { Table, TableBody, TableHeader } from '@patternfly/react-table'
import { parseAndFormatDateTime } from '../../util/date-time'
import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations'
-import { useAuth } from '../../auth'
import TableEmptyState from '../util/TableEmptyState'
const ProjectTable = ({ status, projects, onDelete, isFiltering }) => {
- const { user } = useAuth()
const columns = ['Project name', 'Last edited', 'Access Rights']
const rows =
projects.length > 0
? projects.map((project) => {
- const { level } = project.authorizations.find((auth) => auth.userId === user.sub)
- const Icon = AUTH_ICON_MAP[level]
+ const Icon = AUTH_ICON_MAP[project.role]
return [
{
- title: <Link href={`/projects/${project._id}`}>{project.name}</Link>,
+ title: <Link href={`/projects/${project.id}`}>{project.name}</Link>,
},
- parseAndFormatDateTime(project.datetimeLastEdited),
+ parseAndFormatDateTime(project.updatedAt),
{
title: (
<>
- <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[level]}
+ <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[project.role]}
</>
),
},
diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
index 80099ece..ced5304a 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
@@ -26,26 +26,21 @@ import { Table, TableBody, TableHeader } from '@patternfly/react-table'
import React from 'react'
import TableEmptyState from '../util/TableEmptyState'
import { parseAndFormatDateTime } from '../../util/date-time'
-import { useMutation } from 'react-query'
-import { useProjectTopologies } from '../../data/topology'
+import { useTopologies, useDeleteTopology } from '../../data/topology'
const TopologyTable = ({ projectId }) => {
- const { status, data: topologies = [] } = useProjectTopologies(projectId)
- const { mutate: deleteTopology } = useMutation('deleteTopology')
+ const { status, data: topologies = [] } = useTopologies(projectId)
+ const { mutate: deleteTopology } = useDeleteTopology()
const columns = ['Name', 'Rooms', 'Last Edited']
const rows =
topologies.length > 0
? topologies.map((topology) => [
{
- title: (
- <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}>
- {topology.name}
- </Link>
- ),
+ title: <Link href={`/projects/${projectId}/topologies/${topology.number}`}>{topology.name}</Link>,
},
topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`,
- parseAndFormatDateTime(topology.datetimeLastEdited),
+ parseAndFormatDateTime(topology.updatedAt),
])
: [
{
@@ -69,7 +64,7 @@ const TopologyTable = ({ projectId }) => {
const actionResolver = (_, { rowIndex }) => [
{
title: 'Delete Topology',
- onClick: (_, rowId) => deleteTopology(topologies[rowId]._id),
+ onClick: (_, rowId) => deleteTopology({ projectId, number: topologies[rowId].number }),
isDisabled: rowIndex === 0,
},
]
@@ -89,7 +84,7 @@ const TopologyTable = ({ projectId }) => {
}
TopologyTable.propTypes = {
- projectId: PropTypes.string,
+ projectId: PropTypes.number,
}
export default TopologyTable
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js b/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js
index 9bf369e9..49e5f095 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js
@@ -7,11 +7,11 @@ import { Table, TableBody, TableHeader } from '@patternfly/react-table'
import { deleteRoom } from '../../redux/actions/topology/room'
import TableEmptyState from '../util/TableEmptyState'
-function RoomTable({ topologyId, onSelect }) {
+function RoomTable({ projectId, topologyId, onSelect }) {
const dispatch = useDispatch()
- const { status, data: topology } = useTopology(topologyId)
+ const { status, data: topology } = useTopology(projectId, topologyId)
- const onDelete = (room) => dispatch(deleteRoom(room._id))
+ const onDelete = (room) => dispatch(deleteRoom(room.id))
const columns = ['Name', 'Tiles', 'Racks']
const rows =
@@ -62,7 +62,8 @@ function RoomTable({ topologyId, onSelect }) {
}
RoomTable.propTypes = {
- topologyId: PropTypes.string,
+ projectId: PropTypes.number,
+ topologyId: PropTypes.number,
onSelect: PropTypes.func,
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js b/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js
index 213a4868..f8ee4990 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js
@@ -38,8 +38,8 @@ import { useTopology } from '../../data/topology'
import { parseAndFormatDateTime } from '../../util/date-time'
import RoomTable from './RoomTable'
-function TopologyOverview({ topologyId, onSelect }) {
- const { data: topology } = useTopology(topologyId)
+function TopologyOverview({ projectId, topologyNumber, onSelect }) {
+ const { data: topology } = useTopology(projectId, topologyNumber)
return (
<Grid hasGutter>
<GridItem md={2}>
@@ -57,7 +57,7 @@ function TopologyOverview({ topologyId, onSelect }) {
<DescriptionListTerm>Last edited</DescriptionListTerm>
<DescriptionListDescription>
{topology ? (
- parseAndFormatDateTime(topology.datetimeLastEdited)
+ parseAndFormatDateTime(topology.updatedAt)
) : (
<Skeleton screenreaderText="Loading topology" />
)}
@@ -71,7 +71,11 @@ function TopologyOverview({ topologyId, onSelect }) {
<Card>
<CardTitle>Rooms</CardTitle>
<CardBody>
- <RoomTable topologyId={topologyId} onSelect={(room) => onSelect('room', room)} />
+ <RoomTable
+ projectId={projectId}
+ topologyId={topologyNumber}
+ onSelect={(room) => onSelect('room', room)}
+ />
</CardBody>
</Card>
</GridItem>
@@ -80,7 +84,8 @@ function TopologyOverview({ topologyId, onSelect }) {
}
TopologyOverview.propTypes = {
- topologyId: PropTypes.string,
+ projectId: PropTypes.number,
+ topologyNumber: PropTypes.number,
onSelect: PropTypes.func,
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js
index 411a5ca7..21be3c79 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js
@@ -33,7 +33,7 @@ function TileContainer({ tileId, ...props }) {
const dispatch = useDispatch()
const onClick = (tile) => {
if (tile.rack) {
- dispatch(goFromRoomToRack(tile._id))
+ dispatch(goFromRoomToRack(tile.id))
}
}
return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} />
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js
index 7d304b6b..fdae53f2 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js
@@ -21,6 +21,7 @@ function ImageComponent({ src, x, y, width, height, opacity }) {
}
}, [src])
+ // eslint-disable-next-line jsx-a11y/alt-text
return <Image image={image} x={x} y={y} width={width} height={height} opacity={opacity} />
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js
index 46030135..dad2d62d 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js
@@ -11,8 +11,8 @@ function RackGroup({ tile }) {
<Group>
<TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} />
<Group>
- <RackSpaceFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} />
- <RackEnergyFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} />
+ <RackSpaceFillContainer tileId={tile.id} positionX={tile.positionX} positionY={tile.positionY} />
+ <RackEnergyFillContainer tileId={tile.id} positionX={tile.positionX} positionY={tile.positionY} />
</Group>
</Group>
)
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js
index a42e7bb7..3f8b3089 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js
@@ -7,7 +7,7 @@ import TileContainer from '../TileContainer'
import WallContainer from '../WallContainer'
function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick }) {
- if (currentRoomInConstruction === room._id) {
+ if (currentRoomInConstruction === room.id) {
return (
<Group onClick={onClick}>
{room.tiles.map((tileId) => (
@@ -22,7 +22,7 @@ function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick
{(() => {
if (
(interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') &&
- interactionLevel.roomId === room._id
+ interactionLevel.roomId === room.id
) {
return [
room.tiles
@@ -37,7 +37,7 @@ function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick
return room.tiles.map((tileId) => <TileContainer key={tileId} tileId={tileId} />)
}
})()}
- <WallContainer roomId={room._id} />
+ <WallContainer roomId={room.id} />
</Group>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js
index 5e351691..727f4e25 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js
@@ -40,8 +40,8 @@ function RoomHoverLayer() {
.map((id) => ({ ...state.topology.rooms[id] }))
.filter(
(room) =>
- state.topology.root.rooms.indexOf(room._id) !== -1 &&
- room._id !== state.construction.currentRoomInConstruction
+ state.topology.root.rooms.indexOf(room.id) !== -1 &&
+ room.id !== state.construction.currentRoomInConstruction
)
;[...oldRooms, newRoom].forEach((room) => {
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js
index 9268f615..6f89e10b 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js
@@ -17,7 +17,7 @@ function MachineSidebar({ tileId, position }) {
const rack = topology.racks[topology.tiles[tileId].rack]
return topology.machines[rack.machines[position - 1]]
})
- const machineId = machine._id
+ const machineId = machine.id
return (
<div>
<TextContent>
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
index 88591208..4507b409 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
@@ -22,7 +22,7 @@ function UnitAddComponent({ units, onAdd }) {
selections={selected}
>
{units.map((unit) => (
- <SelectOption value={unit._id} key={unit._id}>
+ <SelectOption value={unit.id} key={unit.id}>
{unit.name}
</SelectOption>
))}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
index 6dcc414f..25e750c4 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
@@ -33,7 +33,7 @@ function UnitListContainer({ machineId, unitType }) {
return machine[unitType].map((id) => state.topology[unitType][id])
})
- const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit._id))
+ const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit.id))
return <UnitListComponent units={units} unitType={unitType} onDelete={onDelete} />
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js
index e944c2e8..6a0c3ff3 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js
@@ -22,14 +22,11 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { useDispatch } from 'react-redux'
import { Button } from '@patternfly/react-core'
import { SaveIcon } from '@patternfly/react-icons'
-import { addPrefab } from '../../../../api/prefabs'
-function AddPrefab({ tileId }) {
- const dispatch = useDispatch()
- const onClick = () => dispatch(addPrefab('name', tileId))
+function AddPrefab() {
+ const onClick = () => {} // TODO
return (
<Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm">
Save this rack to a prefab
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js
index 619bb4e2..e1914730 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js
@@ -43,7 +43,7 @@ function MachineListContainer({ tileId, ...props }) {
<MachineListComponent
{...props}
machines={machinesNull}
- onAdd={(index) => dispatch(addMachine(rack._id, index))}
+ onAdd={(index) => dispatch(addMachine(rack.id, index))}
onSelect={(index) => dispatch(goFromRackToMachine(index))}
/>
)
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js
index 30f38cce..c3422318 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js
@@ -5,11 +5,11 @@ import NameComponent from '../NameComponent'
import { editRackName } from '../../../../redux/actions/topology/rack'
const RackNameContainer = ({ tileId }) => {
- const { name: rackName, _id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+ const { name: rackName, id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
const dispatch = useDispatch()
const callback = (name) => {
if (name) {
- dispatch(editRackName(_id, name))
+ dispatch(editRackName(id, name))
}
}
return <NameComponent name={rackName} onEdit={callback} />
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js
index fb52d826..72d45bea 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js
@@ -27,11 +27,11 @@ import NameComponent from '../NameComponent'
import { editRoomName } from '../../../../redux/actions/topology/room'
function RoomName({ roomId }) {
- const { name: roomName, _id } = useSelector((state) => state.topology.rooms[roomId])
+ const { name: roomName, id } = useSelector((state) => state.topology.rooms[roomId])
const dispatch = useDispatch()
const callback = (name) => {
if (name) {
- dispatch(editRoomName(_id, name))
+ dispatch(editRoomName(id, name))
}
}
return <NameComponent name={roomName} onEdit={callback} />
diff --git a/opendc-web/opendc-web-ui/src/data/experiments.js b/opendc-web/opendc-web-ui/src/data/experiments.js
index a76ea53f..ca8912a2 100644
--- a/opendc-web/opendc-web-ui/src/data/experiments.js
+++ b/opendc-web/opendc-web-ui/src/data/experiments.js
@@ -35,13 +35,13 @@ export function configureExperimentClient(queryClient, auth) {
/**
* Return the available traces to experiment with.
*/
-export function useTraces() {
- return useQuery('traces')
+export function useTraces(options) {
+ return useQuery('traces', options)
}
/**
* Return the available schedulers to experiment with.
*/
-export function useSchedulers() {
- return useQuery('schedulers')
+export function useSchedulers(options) {
+ return useQuery('schedulers', options)
}
diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js
index 9dcd8532..b1db3da5 100644
--- a/opendc-web/opendc-web-ui/src/data/project.js
+++ b/opendc-web/opendc-web-ui/src/data/project.js
@@ -20,9 +20,9 @@
* SOFTWARE.
*/
-import { useQuery } from 'react-query'
+import { useQuery, useMutation } from 'react-query'
import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects'
-import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfoliosOfProject } from '../api/portfolios'
+import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios'
import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios'
/**
@@ -37,79 +37,60 @@ export function configureProjectClient(queryClient, auth) {
mutationFn: (data) => addProject(auth, data),
onSuccess: async (result) => {
queryClient.setQueryData('projects', (old = []) => [...old, result])
- queryClient.setQueryData(['projects', result._id], result)
+ queryClient.setQueryData(['projects', result.id], result)
},
})
queryClient.setMutationDefaults('deleteProject', {
mutationFn: (id) => deleteProject(auth, id),
onSuccess: async (result) => {
- queryClient.setQueryData('projects', (old = []) => old.filter((project) => project._id !== result._id))
- queryClient.removeQueries(['projects', result._id])
+ queryClient.setQueryData('projects', (old = []) => old.filter((project) => project.id !== result.id))
+ queryClient.removeQueries(['projects', result.id])
},
})
queryClient.setQueryDefaults('portfolios', {
- queryFn: ({ queryKey }) => fetchPortfolio(auth, queryKey[1]),
- })
- queryClient.setQueryDefaults('project-portfolios', {
- queryFn: ({ queryKey }) => fetchPortfoliosOfProject(auth, queryKey[1]),
+ queryFn: ({ queryKey }) =>
+ queryKey.length === 2 ? fetchPortfolios(auth, queryKey[1]) : fetchPortfolio(auth, queryKey[1], queryKey[2]),
})
queryClient.setMutationDefaults('addPortfolio', {
- mutationFn: (data) => addPortfolio(auth, data),
+ mutationFn: ({ projectId, ...data }) => addPortfolio(auth, projectId, data),
onSuccess: async (result) => {
- queryClient.setQueryData(['projects', result.projectId], (old) => ({
- ...old,
- portfolioIds: [...old.portfolioIds, result._id],
- }))
- queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => [...old, result])
- queryClient.setQueryData(['portfolios', result._id], result)
+ queryClient.setQueryData(['portfolios', result.project.id], (old = []) => [...old, result])
+ queryClient.setQueryData(['portfolios', result.project.id, result.number], result)
},
})
queryClient.setMutationDefaults('deletePortfolio', {
- mutationFn: (id) => deletePortfolio(auth, id),
+ mutationFn: ({ projectId, number }) => deletePortfolio(auth, projectId, number),
onSuccess: async (result) => {
- queryClient.setQueryData(['projects', result.projectId], (old) => ({
- ...old,
- portfolioIds: old.portfolioIds.filter((id) => id !== result._id),
- }))
- queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) =>
- old.filter((portfolio) => portfolio._id !== result._id)
+ queryClient.setQueryData(['portfolios', result.project.id], (old = []) =>
+ old.filter((portfolio) => portfolio.id !== result.id)
)
- queryClient.removeQueries(['portfolios', result._id])
+ queryClient.removeQueries(['portfolios', result.project.id, result.number])
},
})
queryClient.setQueryDefaults('scenarios', {
- queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1]),
- })
- queryClient.setQueryDefaults('portfolio-scenarios', {
- queryFn: ({ queryKey }) => fetchScenariosOfPortfolio(auth, queryKey[1]),
+ queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1], queryKey[2]),
})
queryClient.setMutationDefaults('addScenario', {
- mutationFn: (data) => addScenario(auth, data),
+ mutationFn: ({ projectId, portfolioNumber, data }) => addScenario(auth, projectId, portfolioNumber, data),
onSuccess: async (result) => {
// Register updated scenario in cache
- queryClient.setQueryData(['scenarios', result._id], result)
- queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => [...old, result])
-
- // Add scenario id to portfolio
- queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({
+ queryClient.setQueryData(['scenarios', result.project.id, result.id], result)
+ queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({
...old,
- scenarioIds: [...old.scenarioIds, result._id],
+ scenarios: [...old.scenarios, result],
}))
},
})
queryClient.setMutationDefaults('deleteScenario', {
- mutationFn: (id) => deleteScenario(auth, id),
+ mutationFn: ({ projectId, number }) => deleteScenario(auth, projectId, number),
onSuccess: async (result) => {
- queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({
+ queryClient.removeQueries(['scenarios', result.project.id, result.id])
+ queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({
...old,
- scenarioIds: old.scenarioIds.filter((id) => id !== result._id),
+ scenarios: old?.scenarios?.filter((scenario) => scenario.id !== result.id),
}))
- queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) =>
- old.filter((scenario) => scenario._id !== result._id)
- )
- queryClient.removeQueries(['scenarios', result._id])
},
})
}
@@ -129,22 +110,57 @@ export function useProject(projectId, options = {}) {
}
/**
+ * Create a mutation for a new project.
+ */
+export function useNewProject() {
+ return useMutation('addProject')
+}
+
+/**
+ * Create a mutation for deleting a project.
+ */
+export function useDeleteProject() {
+ return useMutation('deleteProject')
+}
+
+/**
* Return the portfolio with the specified identifier.
*/
-export function usePortfolio(portfolioId, options = {}) {
- return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId, ...options })
+export function usePortfolio(projectId, portfolioId, options = {}) {
+ return useQuery(['portfolios', projectId, portfolioId], { enabled: !!(projectId && portfolioId), ...options })
}
/**
* Return the portfolios of the specified project.
*/
-export function useProjectPortfolios(projectId, options = {}) {
- return useQuery(['project-portfolios', projectId], { enabled: !!projectId, ...options })
+export function usePortfolios(projectId, options = {}) {
+ return useQuery(['portfolios', projectId], { enabled: !!projectId, ...options })
+}
+
+/**
+ * Create a mutation for a new portfolio.
+ */
+export function useNewPortfolio() {
+ return useMutation('addPortfolio')
+}
+
+/**
+ * Create a mutation for deleting a portfolio.
+ */
+export function useDeletePortfolio() {
+ return useMutation('deletePortfolio')
+}
+
+/**
+ * Create a mutation for a new scenario.
+ */
+export function useNewScenario() {
+ return useMutation('addScenario')
}
/**
- * Return the scenarios of the specified portfolio.
+ * Create a mutation for deleting a scenario.
*/
-export function usePortfolioScenarios(portfolioId, options = {}) {
- return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId, ...options })
+export function useDeleteScenario() {
+ return useMutation('deleteScenario')
}
diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js
index e068ed8e..cf098c56 100644
--- a/opendc-web/opendc-web-ui/src/data/topology.js
+++ b/opendc-web/opendc-web-ui/src/data/topology.js
@@ -20,58 +20,69 @@
* SOFTWARE.
*/
-import { useQuery } from 'react-query'
-import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies'
+import { useQuery, useMutation } from 'react-query'
+import { addTopology, deleteTopology, fetchTopologies, fetchTopology, updateTopology } from '../api/topologies'
/**
* Configure the query defaults for the topology endpoints.
*/
export function configureTopologyClient(queryClient, auth) {
- queryClient.setQueryDefaults('topologies', { queryFn: ({ queryKey }) => fetchTopology(auth, queryKey[1]) })
- queryClient.setQueryDefaults('project-topologies', {
- queryFn: ({ queryKey }) => fetchTopologiesOfProject(auth, queryKey[1]),
+ queryClient.setQueryDefaults('topologies', {
+ queryFn: ({ queryKey }) =>
+ queryKey.length === 2 ? fetchTopologies(auth, queryKey[1]) : fetchTopology(auth, queryKey[1], queryKey[2]),
})
queryClient.setMutationDefaults('addTopology', {
- mutationFn: (data) => addTopology(auth, data),
- onSuccess: async (result) => {
- queryClient.setQueryData(['projects', result.projectId], (old) => ({
- ...old,
- topologyIds: [...old.topologyIds, result._id],
- }))
- queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => [...old, result])
- queryClient.setQueryData(['topologies', result._id], result)
+ mutationFn: ({ projectId, ...data }) => addTopology(auth, projectId, data),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) => [...old, result])
+ queryClient.setQueryData(['topologies', result.project.id, result.number], result)
},
})
queryClient.setMutationDefaults('updateTopology', {
mutationFn: (data) => updateTopology(auth, data),
- onSuccess: (result) => queryClient.setQueryData(['topologies', result._id], result),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) =>
+ old.map((topology) => (topology.id === result.id ? result : topology))
+ )
+ queryClient.setQueryData(['topologies', result.project.id, result.number], result)
+ },
})
queryClient.setMutationDefaults('deleteTopology', {
- mutationFn: (id) => deleteTopology(auth, id),
- onSuccess: async (result) => {
- queryClient.setQueryData(['projects', result.projectId], (old) => ({
- ...old,
- topologyIds: old.topologyIds.filter((id) => id !== result._id),
- }))
- queryClient.setQueryData(['project-topologies', result.projectId], (old = []) =>
- old.filter((topology) => topology._id !== result._id)
+ mutationFn: ({ projectId, id }) => deleteTopology(auth, projectId, id),
+ onSuccess: (result) => {
+ queryClient.setQueryData(['topologies', result.project.id], (old = []) =>
+ old.filter((topology) => topology.id !== result.id)
)
- queryClient.removeQueries(['topologies', result._id])
+ queryClient.removeQueries(['topologies', result.project.id, result.number])
},
})
}
/**
- * Return the current active topology.
+ * Fetch the topology with the specified identifier for the specified project.
+ */
+export function useTopology(projectId, topologyId, options = {}) {
+ return useQuery(['topologies', projectId, topologyId], { enabled: !!(projectId && topologyId), ...options })
+}
+
+/**
+ * Fetch all topologies of the specified project.
+ */
+export function useTopologies(projectId, options = {}) {
+ return useQuery(['topologies', projectId], { enabled: !!projectId, ...options })
+}
+
+/**
+ * Create a mutation for a new topology.
*/
-export function useTopology(topologyId, options = {}) {
- return useQuery(['topologies', topologyId], { enabled: !!topologyId, ...options })
+export function useNewTopology() {
+ return useMutation('addTopology')
}
/**
- * Return the topologies of the specified project.
+ * Create a mutation for deleting a topology.
*/
-export function useProjectTopologies(projectId, options = {}) {
- return useQuery(['project-topologies', projectId], { enabled: !!projectId, ...options })
+export function useDeleteTopology() {
+ return useMutation('deleteTopology')
}
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
index 900ff405..4861f5c1 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -22,6 +22,7 @@
import PropTypes from 'prop-types'
import Head from 'next/head'
+import Script from 'next/script'
import { Provider } from 'react-redux'
import { useNewQueryClient } from '../data/query'
import { useStore } from '../redux'
@@ -91,6 +92,19 @@ export default function App(props) {
<Inner {...props} />
</AuthProvider>
</Sentry.ErrorBoundary>
+ {/* Google Analytics */}
+ <Script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" />
+ <Script
+ id="gtag"
+ dangerouslySetInnerHTML={{
+ __html: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'UA-84285092-3');
+ `,
+ }}
+ />
</>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/_document.js b/opendc-web/opendc-web-ui/src/pages/_document.js
index 51d8d3e0..011bf4da 100644
--- a/opendc-web/opendc-web-ui/src/pages/_document.js
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -69,19 +69,6 @@ class OpenDCDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
rel="stylesheet"
/>
-
- {/* Google Analytics */}
- <script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" />
- <script
- dangerouslySetInnerHTML={{
- __html: `
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'UA-84285092-3');
- `,
- }}
- />
</Head>
<body>
<Main />
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
index c07a2c31..39fcb4f3 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -40,9 +40,9 @@ import BreadcrumbLink from '../../../components/util/BreadcrumbLink'
function Project() {
const router = useRouter()
- const { project: projectId } = router.query
+ const projectId = +router.query['project']
- const { data: project } = useProject(projectId)
+ const { data: project } = useProject(+projectId)
const breadcrumb = (
<Breadcrumb>
@@ -57,7 +57,7 @@ function Project() {
const contextSelectors = (
<ContextSelectionSection>
- <ProjectSelector projectId={projectId} />
+ <ProjectSelector activeProject={project} />
</ContextSelectionSection>
)
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
index d1533d98..68345d0b 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
@@ -20,6 +20,7 @@
* SOFTWARE.
*/
+import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import Head from 'next/head'
import React, { useRef } from 'react'
@@ -42,18 +43,24 @@ import PortfolioSelector from '../../../../components/context/PortfolioSelector'
import ProjectSelector from '../../../../components/context/ProjectSelector'
import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
import PortfolioOverview from '../../../../components/portfolios/PortfolioOverview'
-import PortfolioResults from '../../../../components/portfolios/PortfolioResults'
+import { usePortfolio } from '../../../../data/project'
+
+const PortfolioResults = dynamic(() => import('../../../../components/portfolios/PortfolioResults'))
/**
* Page that displays the results in a portfolio.
*/
function Portfolio() {
const router = useRouter()
- const { project: projectId, portfolio: portfolioId } = router.query
+ const projectId = +router.query['project']
+ const portfolioNumber = +router.query['portfolio']
const overviewRef = useRef(null)
const resultsRef = useRef(null)
+ const { data: portfolio } = usePortfolio(projectId, portfolioNumber)
+ const project = portfolio?.project
+
const breadcrumb = (
<Breadcrumb>
<BreadcrumbItem to="/projects" component={BreadcrumbLink}>
@@ -62,7 +69,11 @@ function Portfolio() {
<BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}>
Project details
</BreadcrumbItem>
- <BreadcrumbItem to={`/projects/${projectId}/portfolios/${portfolioId}`} component={BreadcrumbLink} isActive>
+ <BreadcrumbItem
+ to={`/projects/${projectId}/portfolios/${portfolioNumber}`}
+ component={BreadcrumbLink}
+ isActive
+ >
Portfolio
</BreadcrumbItem>
</Breadcrumb>
@@ -70,8 +81,8 @@ function Portfolio() {
const contextSelectors = (
<ContextSelectionSection>
- <ProjectSelector projectId={projectId} />
- <PortfolioSelector projectId={projectId} portfolioId={portfolioId} />
+ <ProjectSelector activeProject={project} />
+ <PortfolioSelector activePortfolio={portfolio} />
</ContextSelectionSection>
)
@@ -104,10 +115,10 @@ function Portfolio() {
</PageSection>
<PageSection isFilled>
<TabContent eventKey={0} id="overview" ref={overviewRef} aria-label="Overview tab">
- <PortfolioOverview portfolioId={portfolioId} />
+ <PortfolioOverview projectId={projectId} portfolioId={portfolioNumber} />
</TabContent>
<TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden>
- <PortfolioResults portfolioId={portfolioId} />
+ <PortfolioResults projectId={projectId} portfolioId={portfolioNumber} />
</TabContent>
</PageSection>
</AppPage>
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
index f7188d9f..6297b8c3 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
@@ -26,7 +26,6 @@ import ContextSelectionSection from '../../../../components/context/ContextSelec
import ProjectSelector from '../../../../components/context/ProjectSelector'
import TopologySelector from '../../../../components/context/TopologySelector'
import TopologyOverview from '../../../../components/topologies/TopologyOverview'
-import { useProject } from '../../../../data/project'
import { useDispatch } from 'react-redux'
import React, { useEffect, useState } from 'react'
import Head from 'next/head'
@@ -45,6 +44,7 @@ import {
TextContent,
} from '@patternfly/react-core'
import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
+import { useTopology } from '../../../../data/topology'
import { goToRoom } from '../../../../redux/actions/interaction-level'
import { openTopology } from '../../../../redux/actions/topology'
@@ -55,16 +55,18 @@ const TopologyMap = dynamic(() => import('../../../../components/topologies/Topo
*/
function Topology() {
const router = useRouter()
- const { project: projectId, topology: topologyId } = router.query
+ const projectId = +router.query['project']
+ const topologyNumber = +router.query['topology']
- const { data: project } = useProject(projectId)
+ const { data: topology } = useTopology(projectId, topologyNumber)
+ const project = topology?.project
const dispatch = useDispatch()
useEffect(() => {
- if (topologyId) {
- dispatch(openTopology(topologyId))
+ if (topologyNumber) {
+ dispatch(openTopology(projectId, topologyNumber))
}
- }, [topologyId, dispatch])
+ }, [projectId, topologyNumber, dispatch])
const [activeTab, setActiveTab] = useState('overview')
@@ -76,7 +78,11 @@ function Topology() {
<BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}>
Project details
</BreadcrumbItem>
- <BreadcrumbItem to={`/projects/${projectId}/topologies/${topologyId}`} component={BreadcrumbLink} isActive>
+ <BreadcrumbItem
+ to={`/projects/${projectId}/topologies/${topologyNumber}`}
+ component={BreadcrumbLink}
+ isActive
+ >
Topology
</BreadcrumbItem>
</Breadcrumb>
@@ -84,8 +90,8 @@ function Topology() {
const contextSelectors = (
<ContextSelectionSection>
- <ProjectSelector projectId={projectId} />
- <TopologySelector projectId={projectId} topologyId={topologyId} />
+ <ProjectSelector activeProject={project} />
+ <TopologySelector activeTopology={topology} />
</ContextSelectionSection>
)
@@ -117,16 +123,22 @@ function Topology() {
<PageSection padding={activeTab === 'floor-plan' && { default: 'noPadding' }} isFilled>
<TabContent id="overview" aria-label="Overview tab" hidden={activeTab !== 'overview'}>
<TopologyOverview
- topologyId={topologyId}
+ projectId={projectId}
+ topologyNumber={topologyNumber}
onSelect={(type, obj) => {
if (type === 'room') {
- dispatch(goToRoom(obj._id))
+ dispatch(goToRoom(obj.id))
setActiveTab('floor-plan')
}
}}
/>
</TabContent>
- <TabContent id="floor-plan" aria-label="Floor Plan tab" className="pf-u-h-100" hidden={activeTab !== 'floor-plan'}>
+ <TabContent
+ id="floor-plan"
+ aria-label="Floor Plan tab"
+ className="pf-u-h-100"
+ hidden={activeTab !== 'floor-plan'}
+ >
<TopologyMap />
</TabContent>
</PageSection>
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js
index eb77701e..bb1fbd69 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -26,9 +26,8 @@ import ProjectFilterPanel from '../../components/projects/FilterPanel'
import { useAuth } from '../../auth'
import { AppPage } from '../../components/AppPage'
import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core'
-import { useProjects } from '../../data/project'
+import { useProjects, useDeleteProject } from '../../data/project'
import ProjectTable from '../../components/projects/ProjectTable'
-import { useMutation } from 'react-query'
import NewProject from '../../components/projects/NewProject'
const getVisibleProjects = (projects, filter, userId) => {
@@ -52,13 +51,12 @@ function Projects() {
const { user } = useAuth()
const { status, data: projects } = useProjects()
const [filter, setFilter] = useState('SHOW_ALL')
- const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter, user?.sub), [
- projects,
- filter,
- user?.sub,
- ])
+ const visibleProjects = useMemo(
+ () => getVisibleProjects(projects ?? [], filter, user?.sub),
+ [projects, filter, user?.sub]
+ )
- const { mutate: deleteProject } = useMutation('deleteProject')
+ const { mutate: deleteProject } = useDeleteProject()
return (
<AppPage>
@@ -76,7 +74,7 @@ function Projects() {
status={status}
isFiltering={filter !== 'SHOW_ALL'}
projects={visibleProjects}
- onDelete={(project) => deleteProject(project._id)}
+ onDelete={(project) => deleteProject(project.id)}
/>
<NewProject />
</PageSection>
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
index 939c24a4..e430da2e 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
@@ -14,16 +14,16 @@ export const DELETE_TILE = 'DELETE_TILE'
export function startNewRoomConstruction() {
return (dispatch, getState) => {
const { topology } = getState()
- const topologyId = topology.root._id
+ const topologyId = topology.root.id
const room = {
- _id: uuid(),
+ id: uuid(),
name: 'Room',
topologyId,
tiles: [],
}
dispatch(addRoom(topologyId, room))
- dispatch(startNewRoomConstructionSucceeded(room._id))
+ dispatch(startNewRoomConstructionSucceeded(room.id))
}
}
@@ -97,7 +97,7 @@ export function addTile(roomId, positionX, positionY) {
return {
type: ADD_TILE,
tile: {
- _id: uuid(),
+ id: uuid(),
roomId,
positionX,
positionY,
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js
index 94a712c4..d48af37a 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js
@@ -23,9 +23,10 @@
export const OPEN_TOPOLOGY = 'OPEN_TOPOLOGY'
export const STORE_TOPOLOGY = 'STORE_TOPOLOGY'
-export function openTopology(id) {
+export function openTopology(projectId, id) {
return {
type: OPEN_TOPOLOGY,
+ projectId,
id,
}
}
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js
index c319d966..308acaa6 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js
@@ -24,7 +24,7 @@ export function addMachine(rackId, position) {
return {
type: ADD_MACHINE,
machine: {
- _id: uuid(),
+ id: uuid(),
rackId,
position,
cpus: [],
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
index bd447db5..fd2d8cdc 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
@@ -16,7 +16,7 @@ export function addRoom(topologyId, room) {
return {
type: ADD_ROOM,
room: {
- _id: uuid(),
+ id: uuid(),
topologyId,
...room,
},
@@ -54,9 +54,9 @@ export function addRackToTile(positionX, positionY) {
dispatch({
type: ADD_RACK_TO_TILE,
rack: {
- _id: uuid(),
+ id: uuid(),
name: 'Rack',
- tileId: tile._id,
+ tileId: tile.id,
capacity: DEFAULT_RACK_SLOT_CAPACITY,
powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
machines: [],
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js
index 47af53cf..1789257b 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js
@@ -10,7 +10,7 @@ function machine(state = {}, action, { racks }) {
case ADD_MACHINE:
return produce(state, (draft) => {
const { machine } = action
- draft[machine._id] = machine
+ draft[machine.id] = machine
})
case DELETE_MACHINE:
return produce(state, (draft) => {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js
index 155837cb..ca79348a 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js
@@ -33,7 +33,7 @@ function rack(state = {}, action, { machines }) {
case ADD_RACK_TO_TILE:
return produce(state, (draft) => {
const { rack } = action
- draft[rack._id] = rack
+ draft[rack.id] = rack
})
case EDIT_RACK_NAME:
return produce(state, (draft) => {
@@ -48,7 +48,7 @@ function rack(state = {}, action, { machines }) {
case ADD_MACHINE:
return produce(state, (draft) => {
const { machine } = action
- draft[machine.rackId].machines.push(machine._id)
+ draft[machine.rackId].machines.push(machine.id)
})
case DELETE_MACHINE:
return produce(state, (draft) => {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js
index d6cc51c1..c05c8bfa 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js
@@ -32,7 +32,7 @@ function room(state = {}, action, { tiles }) {
case ADD_ROOM:
return produce(state, (draft) => {
const { room } = action
- draft[room._id] = room
+ draft[room.id] = room
})
case DELETE_ROOM:
return produce(state, (draft) => {
@@ -47,7 +47,7 @@ function room(state = {}, action, { tiles }) {
case ADD_TILE:
return produce(state, (draft) => {
const { tile } = action
- draft[tile.roomId].tiles.push(tile._id)
+ draft[tile.roomId].tiles.push(tile.id)
})
case DELETE_TILE:
return produce(state, (draft) => {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js
index 6dbccb66..8e5ecd6e 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js
@@ -33,7 +33,7 @@ function tile(state = {}, action, { racks }) {
case ADD_TILE:
return produce(state, (draft) => {
const { tile } = action
- draft[tile._id] = tile
+ draft[tile.id] = tile
})
case DELETE_TILE:
return produce(state, (draft) => {
@@ -43,7 +43,7 @@ function tile(state = {}, action, { racks }) {
case ADD_RACK_TO_TILE:
return produce(state, (draft) => {
const { rack } = action
- draft[rack.tileId].rack = rack._id
+ draft[rack.tileId].rack = rack.id
})
case DELETE_RACK:
return produce(state, (draft) => {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js
index cd9b5efd..dff0a69e 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js
@@ -21,7 +21,7 @@
*/
import produce from 'immer'
-import { STORE_TOPOLOGY } from "../../actions/topology";
+import { STORE_TOPOLOGY } from '../../actions/topology'
import { ADD_ROOM, DELETE_ROOM } from '../../actions/topology/room'
function topology(state = undefined, action) {
@@ -31,7 +31,7 @@ function topology(state = undefined, action) {
case ADD_ROOM:
return produce(state, (draft) => {
const { room } = action
- draft.rooms.push(room._id)
+ draft.rooms.push(room.id)
})
case DELETE_ROOM:
return produce(state, (draft) => {
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
index 4c8ff5da..15147bcf 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
@@ -32,14 +32,15 @@ export function* updateServer() {
* Watch the topology on the server for changes.
*/
export function* watchServer() {
- let { id } = yield take(OPEN_TOPOLOGY)
+ let { projectId, id } = yield take(OPEN_TOPOLOGY)
while (true) {
- const channel = yield queryObserver(id)
+ const channel = yield queryObserver(projectId, id)
while (true) {
const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)])
if (action) {
+ projectId = action.projectId
id = action.id
break
}
@@ -57,9 +58,9 @@ export function* watchServer() {
/**
* Observe changes for the topology with the specified identifier.
*/
-function* queryObserver(id) {
+function* queryObserver(projectId, id) {
const queryClient = yield getContext('queryClient')
- const observer = new QueryObserver(queryClient, { queryKey: ['topologies', id] })
+ const observer = new QueryObserver(queryClient, { queryKey: ['topologies', projectId, id] })
return eventChannel((emitter) => {
const unsubscribe = observer.subscribe((result) => {
diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js
index abdf146e..6c93f458 100644
--- a/opendc-web/opendc-web-ui/src/shapes.js
+++ b/opendc-web/opendc-web-ui/src/shapes.js
@@ -22,26 +22,18 @@
import PropTypes from 'prop-types'
-export const User = PropTypes.shape({
- _id: PropTypes.string.isRequired,
- googleId: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- givenName: PropTypes.string.isRequired,
- familyName: PropTypes.string.isRequired,
- authorizations: PropTypes.array.isRequired,
-})
+export const ProjectRole = PropTypes.oneOf(['VIEWER', 'EDITOR', 'OWNER'])
export const Project = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
- datetimeCreated: PropTypes.string.isRequired,
- datetimeLastEdited: PropTypes.string.isRequired,
- topologyIds: PropTypes.array.isRequired,
- portfolioIds: PropTypes.array.isRequired,
+ createdAt: PropTypes.string.isRequired,
+ updatedAt: PropTypes.string.isRequired,
+ role: ProjectRole,
})
export const ProcessingUnit = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
clockRateMhz: PropTypes.number.isRequired,
numberOfCores: PropTypes.number.isRequired,
@@ -49,7 +41,7 @@ export const ProcessingUnit = PropTypes.shape({
})
export const StorageUnit = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
speedMbPerS: PropTypes.number.isRequired,
sizeMb: PropTypes.number.isRequired,
@@ -57,38 +49,45 @@ export const StorageUnit = PropTypes.shape({
})
export const Machine = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
position: PropTypes.number.isRequired,
- cpus: PropTypes.arrayOf(PropTypes.string),
- gpus: PropTypes.arrayOf(PropTypes.string),
- memories: PropTypes.arrayOf(PropTypes.string),
- storages: PropTypes.arrayOf(PropTypes.string),
+ cpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])),
+ gpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])),
+ memories: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])),
+ storages: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])),
})
export const Rack = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
capacity: PropTypes.number.isRequired,
powerCapacityW: PropTypes.number.isRequired,
- machines: PropTypes.arrayOf(PropTypes.string),
+ machines: PropTypes.arrayOf(PropTypes.oneOfType([Machine, PropTypes.string])),
})
export const Tile = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
positionX: PropTypes.number.isRequired,
positionY: PropTypes.number.isRequired,
- rack: PropTypes.string,
+ rack: PropTypes.oneOfType([Rack, PropTypes.string]),
})
export const Room = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
- tiles: PropTypes.arrayOf(PropTypes.string),
+ tiles: PropTypes.arrayOf(PropTypes.oneOfType([Tile, PropTypes.string])),
})
export const Topology = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
name: PropTypes.string.isRequired,
- rooms: PropTypes.arrayOf(PropTypes.string),
+ rooms: PropTypes.arrayOf(PropTypes.oneOfType([Room, PropTypes.string])),
+})
+
+export const Phenomena = PropTypes.shape({
+ failures: PropTypes.bool.isRequired,
+ interference: PropTypes.bool.isRequired,
})
export const Scheduler = PropTypes.shape({
@@ -96,47 +95,82 @@ export const Scheduler = PropTypes.shape({
})
export const Trace = PropTypes.shape({
- _id: PropTypes.string.isRequired,
+ id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
})
-export const Portfolio = PropTypes.shape({
- _id: PropTypes.string.isRequired,
- projectId: PropTypes.string.isRequired,
+export const Workload = PropTypes.shape({
+ trace: Trace.isRequired,
+ samplingFraction: PropTypes.number.isRequired,
+})
+
+export const Targets = PropTypes.shape({
+ repeats: PropTypes.number.isRequired,
+ metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+})
+
+export const TopologySummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ name: PropTypes.string.isRequired,
+})
+
+export const PortfolioSummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
name: PropTypes.string.isRequired,
- scenarioIds: PropTypes.arrayOf(PropTypes.string).isRequired,
targets: PropTypes.shape({
- enabledMetrics: PropTypes.arrayOf(PropTypes.string).isRequired,
- repeatsPerScenario: PropTypes.number.isRequired,
+ repeats: PropTypes.number.isRequired,
+ metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
}).isRequired,
})
-export const Scenario = PropTypes.shape({
- _id: PropTypes.string.isRequired,
- portfolioId: PropTypes.string.isRequired,
+export const ScenarioSummary = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
- simulation: PropTypes.shape({
- state: PropTypes.string.isRequired,
- }).isRequired,
- trace: PropTypes.shape({
- traceId: PropTypes.string.isRequired,
- trace: Trace,
- loadSamplingFraction: PropTypes.number.isRequired,
- }).isRequired,
- topology: PropTypes.shape({
- topologyId: PropTypes.string.isRequired,
- topology: Topology,
- }).isRequired,
- operational: PropTypes.shape({
- failuresEnabled: PropTypes.bool.isRequired,
- performanceInterferenceEnabled: PropTypes.bool.isRequired,
- schedulerName: PropTypes.string.isRequired,
- scheduler: Scheduler,
- }).isRequired,
+ workload: Workload.isRequired,
+ topology: TopologySummary.isRequired,
+ phenomena: Phenomena.isRequired,
+ schedulerName: PropTypes.string.isRequired,
results: PropTypes.object,
})
+export const JobState = PropTypes.oneOf(['PENDING', 'CLAIMED', 'RUNNING', 'FAILED', 'FINISHED'])
+
+export const Job = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ state: JobState.isRequired,
+ createdAt: PropTypes.string.isRequired,
+ updatedAt: PropTypes.string.isRequired,
+ results: PropTypes.object,
+})
+
+export const Scenario = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ project: Project.isRequired,
+ portfolio: PortfolioSummary.isRequired,
+ name: PropTypes.string.isRequired,
+ workload: Workload.isRequired,
+ topology: TopologySummary.isRequired,
+ phenomena: Phenomena.isRequired,
+ schedulerName: PropTypes.string.isRequired,
+ job: Job.isRequired,
+})
+
+export const Portfolio = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ number: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ project: Project.isRequired,
+ targets: Targets.isRequired,
+ scenarios: PropTypes.arrayOf(ScenarioSummary).isRequired,
+})
+
export const WallSegment = PropTypes.shape({
startPosX: PropTypes.number.isRequired,
startPosY: PropTypes.number.isRequired,
diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js
index ce5d34b6..fffcefeb 100644
--- a/opendc-web/opendc-web-ui/src/util/authorizations.js
+++ b/opendc-web/opendc-web-ui/src/util/authorizations.js
@@ -3,13 +3,13 @@ import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon'
import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon'
export const AUTH_ICON_MAP = {
- OWN: HomeIcon,
- EDIT: EditIcon,
- VIEW: EyeIcon,
+ OWNER: HomeIcon,
+ EDITOR: EditIcon,
+ VIEWER: EyeIcon,
}
export const AUTH_DESCRIPTION_MAP = {
- OWN: 'Own',
- EDIT: 'Can Edit',
- VIEW: 'Can View',
+ OWNER: 'Own',
+ EDITOR: 'Can Edit',
+ VIEWER: 'Can View',
}
diff --git a/opendc-web/opendc-web-ui/src/util/topology-schema.js b/opendc-web/opendc-web-ui/src/util/topology-schema.js
index 7779ccfe..ff672dd6 100644
--- a/opendc-web/opendc-web-ui/src/util/topology-schema.js
+++ b/opendc-web/opendc-web-ui/src/util/topology-schema.js
@@ -22,10 +22,10 @@
import { schema } from 'normalizr'
-const Cpu = new schema.Entity('cpus', {}, { idAttribute: '_id' })
-const Gpu = new schema.Entity('gpus', {}, { idAttribute: '_id' })
-const Memory = new schema.Entity('memories', {}, { idAttribute: '_id' })
-const Storage = new schema.Entity('storages', {}, { idAttribute: '_id' })
+const Cpu = new schema.Entity('cpus', {}, { idAttribute: 'id' })
+const Gpu = new schema.Entity('gpus', {}, { idAttribute: 'id' })
+const Memory = new schema.Entity('memories', {}, { idAttribute: 'id' })
+const Storage = new schema.Entity('storages', {}, { idAttribute: 'id' })
export const Machine = new schema.Entity(
'machines',
@@ -35,13 +35,13 @@ export const Machine = new schema.Entity(
memories: [Memory],
storages: [Storage],
},
- { idAttribute: '_id' }
+ { idAttribute: 'id' }
)
-export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: '_id' })
+export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: 'id' })
-export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: '_id' })
+export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: 'id' })
-export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: '_id' })
+export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: 'id' })
-export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: '_id' })
+export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: 'id' })
diff --git a/opendc-web/opendc-web-ui/src/util/unit-specifications.js b/opendc-web/opendc-web-ui/src/util/unit-specifications.js
index 28479edd..3e3671cd 100644
--- a/opendc-web/opendc-web-ui/src/util/unit-specifications.js
+++ b/opendc-web/opendc-web-ui/src/util/unit-specifications.js
@@ -1,34 +1,34 @@
export const CPU_UNITS = {
'cpu-1': {
- _id: 'cpu-1',
+ id: 'cpu-1',
name: 'Intel i7 v6 6700k',
clockRateMhz: 4100,
numberOfCores: 4,
energyConsumptionW: 70,
},
'cpu-2': {
- _id: 'cpu-2',
+ id: 'cpu-2',
name: 'Intel i5 v6 6700k',
clockRateMhz: 3500,
numberOfCores: 2,
energyConsumptionW: 50,
},
'cpu-3': {
- _id: 'cpu-3',
+ id: 'cpu-3',
name: 'Intel® Xeon® E-2224G',
clockRateMhz: 3500,
numberOfCores: 4,
energyConsumptionW: 71,
},
'cpu-4': {
- _id: 'cpu-4',
+ id: 'cpu-4',
name: 'Intel® Xeon® E-2244G',
clockRateMhz: 3800,
numberOfCores: 8,
energyConsumptionW: 71,
},
'cpu-5': {
- _id: 'cpu-5',
+ id: 'cpu-5',
name: 'Intel® Xeon® E-2246G',
clockRateMhz: 3600,
numberOfCores: 12,
@@ -38,14 +38,14 @@ export const CPU_UNITS = {
export const GPU_UNITS = {
'gpu-1': {
- _id: 'gpu-1',
+ id: 'gpu-1',
name: 'NVIDIA GTX 4 1080',
clockRateMhz: 1200,
numberOfCores: 200,
energyConsumptionW: 250,
},
'gpu-2': {
- _id: 'gpu-2',
+ id: 'gpu-2',
name: 'NVIDIA Tesla V100',
clockRateMhz: 1200,
numberOfCores: 5120,
@@ -55,28 +55,28 @@ export const GPU_UNITS = {
export const MEMORY_UNITS = {
'memory-1': {
- _id: 'memory-1',
+ id: 'memory-1',
name: 'Samsung PC DRAM K4A4G045WD',
speedMbPerS: 16000,
sizeMb: 4000,
energyConsumptionW: 10,
},
'memory-2': {
- _id: 'memory-2',
+ id: 'memory-2',
name: 'Samsung PC DRAM M393A2K43BB1-CRC',
speedMbPerS: 2400,
sizeMb: 16000,
energyConsumptionW: 10,
},
'memory-3': {
- _id: 'memory-3',
+ id: 'memory-3',
name: 'Crucial MTA18ASF4G72PDZ-3G2E1',
speedMbPerS: 3200,
sizeMb: 32000,
energyConsumptionW: 10,
},
'memory-4': {
- _id: 'memory-4',
+ id: 'memory-4',
name: 'Crucial MTA9ASF2G72PZ-3G2E1',
speedMbPerS: 3200,
sizeMb: 16000,
@@ -86,14 +86,14 @@ export const MEMORY_UNITS = {
export const STORAGE_UNITS = {
'storage-1': {
- _id: 'storage-1',
+ id: 'storage-1',
name: 'Samsung EVO 2016 SATA III',
speedMbPerS: 6000,
sizeMb: 250000,
energyConsumptionW: 10,
},
'storage-2': {
- _id: 'storage-2',
+ id: 'storage-2',
name: 'Western Digital MTA9ASF2G72PZ-3G2E1',
speedMbPerS: 6000,
sizeMb: 4000000,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f518a984..267c8edd 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -35,7 +35,9 @@ include(":opendc-faas:opendc-faas-simulator")
include(":opendc-experiments:opendc-experiments-capelin")
include(":opendc-experiments:opendc-experiments-serverless20")
include(":opendc-experiments:opendc-experiments-tf20")
+include(":opendc-web:opendc-web-proto")
include(":opendc-web:opendc-web-api")
+include(":opendc-web:opendc-web-client")
include(":opendc-web:opendc-web-ui")
include(":opendc-web:opendc-web-runner")
include(":opendc-simulator:opendc-simulator-core")
@@ -61,5 +63,4 @@ include(":opendc-harness:opendc-harness-engine")
include(":opendc-harness:opendc-harness-cli")
include(":opendc-harness:opendc-harness-junit5")
-enableFeaturePreview("VERSION_CATALOGS")
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")