summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server/src/main
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-08-03 14:42:49 +0200
committerGitHub <noreply@github.com>2022-08-03 14:42:49 +0200
commit2692a032b8ecbe2ddccfb88628cf37af56c3ea36 (patch)
tree5dde4331c0a10dd8e843c75cf68d2c7d3cd45906 /opendc-web/opendc-web-server/src/main
parentb023b085425c0d0f7952c2c331576b8d0fc8c857 (diff)
parent87df18bb691260ee69d2e48cf1598e6a4acc329b (diff)
merge: Simplify OpenDC deployment process
This pull request attempts to simplify the deployment process necessary for deploying OpenDC locally or using Docker. There was currently not a clear and simple way to deploy OpenDC locally, yet the Docker-based deployment was also out-of-sync. ## Implementation Notes :hammer_and_pick: * Address several bugs in the OpenDC web runner * Fix dependency related issues * Rename `opendc-web-api` to `opendc-web-server` * Create a local distribution of `opendc-web-server`. * Fix Docker deployment * Update deployment guide ## External Dependencies :four_leaf_clover: * Quarkus 2.11.1 * `jandex-gradle-plugin` 0.13.2 ## Breaking API Changes :warning: * `TraceFormat.installedProviders` has been changed to `TraceFormat.getInstalledProviders`. The list of installed providers is now not cached at first access, but queried every time the method is invoked and its results depend on the caller context (e.g., context class loader). * `OpenDCRunner` now requires a `JobManager` as constructor argument, which can be constructed as follows: `JobManager.create(client)` * `opendc-web-api` is now renamed to `opendc-web-server`.
Diffstat (limited to 'opendc-web/opendc-web-server/src/main')
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt30
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt95
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt89
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt134
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt58
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt38
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt107
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt92
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt58
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt39
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt93
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt76
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt157
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt90
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt86
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt53
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt48
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt51
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt45
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt43
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt72
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt77
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt59
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt82
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt60
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt88
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt81
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt104
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt86
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt69
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt128
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt127
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt48
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt120
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt40
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt51
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt38
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt74
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt26
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt83
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt107
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt38
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt48
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt149
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt67
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/META-INF/branding/logo.pngbin0 -> 2825 bytes
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-dev.properties37
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-docker.properties49
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-prod.properties37
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-test.properties37
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application.properties48
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql144
52 files changed, 3756 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt
new file mode 100644
index 00000000..1a426095
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server
+
+import javax.ws.rs.core.Application
+
+/**
+ * [Application] definition for the OpenDC web API.
+ */
+class OpenDCApplication : Application()
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
new file mode 100644
index 00000000..024e7b89
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.proto.JobState
+import org.opendc.web.server.util.hibernate.json.JsonType
+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-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt
new file mode 100644
index 00000000..3e3f76a0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.proto.Targets
+import org.opendc.web.server.util.hibernate.json.JsonType
+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-server/src/main/kotlin/org/opendc/web/server/model/Project.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt
new file mode 100644
index 00000000..aa98b677
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt
new file mode 100644
index 00000000..a353186e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt
new file mode 100644
index 00000000..449b6608
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt
new file mode 100644
index 00000000..e40cff47
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.proto.OperationalPhenomena
+import org.opendc.web.server.util.hibernate.json.JsonType
+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-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt
new file mode 100644
index 00000000..a190b1ee
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.model
+
+import org.hibernate.annotations.Type
+import org.hibernate.annotations.TypeDef
+import org.opendc.web.proto.Room
+import org.opendc.web.server.util.hibernate.json.JsonType
+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-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt
new file mode 100644
index 00000000..8aaac613
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt
new file mode 100644
index 00000000..9c59dc25
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
new file mode 100644
index 00000000..5fee07a3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.proto.JobState
+import org.opendc.web.server.model.Job
+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-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt
new file mode 100644
index 00000000..77130c15
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt
new file mode 100644
index 00000000..519da3de
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.server.model.Project
+import org.opendc.web.server.model.ProjectAuthorization
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt
new file mode 100644
index 00000000..145db71d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt
new file mode 100644
index 00000000..e8eadd63
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt
new file mode 100644
index 00000000..f328eea6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.repository
+
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt
new file mode 100644
index 00000000..919b25fc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt
new file mode 100644
index 00000000..f46f7f91
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest
+
+import org.opendc.web.proto.Trace
+import org.opendc.web.server.service.TraceService
+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-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt
new file mode 100644
index 00000000..d8df72e0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt
new file mode 100644
index 00000000..e50917aa
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
new file mode 100644
index 00000000..351a2237
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.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.server.rest.runner
+
+import org.opendc.web.proto.runner.Job
+import org.opendc.web.server.service.JobService
+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 try {
+ jobService.updateState(id, update.state, update.results)
+ ?: throw WebApplicationException("Job not found", 404)
+ } catch (e: IllegalArgumentException) {
+ throw WebApplicationException(e, 400)
+ } catch (e: IllegalStateException) {
+ throw WebApplicationException(e, 409)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt
new file mode 100644
index 00000000..352dd491
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.Portfolio
+import org.opendc.web.server.service.PortfolioService
+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-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt
new file mode 100644
index 00000000..f2372bde
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.Scenario
+import org.opendc.web.server.service.ScenarioService
+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-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt
new file mode 100644
index 00000000..f3d96f55
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.Project
+import org.opendc.web.server.service.ProjectService
+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-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt
new file mode 100644
index 00000000..24cdcb6a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.Scenario
+import org.opendc.web.server.service.ScenarioService
+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-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt
new file mode 100644
index 00000000..40b3741c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.Topology
+import org.opendc.web.server.service.TopologyService
+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-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
new file mode 100644
index 00000000..6b49e8b6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.JobState
+import org.opendc.web.proto.runner.Job
+import org.opendc.web.server.repository.JobRepository
+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-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt
new file mode 100644
index 00000000..0d380190
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.user.Portfolio
+import org.opendc.web.server.model.*
+import org.opendc.web.server.repository.PortfolioRepository
+import org.opendc.web.server.repository.ProjectRepository
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt
new file mode 100644
index 00000000..44348195
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.user.Project
+import org.opendc.web.proto.user.ProjectRole
+import org.opendc.web.server.model.*
+import org.opendc.web.server.repository.ProjectRepository
+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-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
new file mode 100644
index 00000000..1dcc95ee
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.server.model.Job
+import org.opendc.web.server.model.Portfolio
+import org.opendc.web.server.model.Scenario
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
new file mode 100644
index 00000000..5b56068d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.user.Scenario
+import org.opendc.web.server.model.*
+import org.opendc.web.server.repository.*
+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-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt
new file mode 100644
index 00000000..5c2a457a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.user.Topology
+import org.opendc.web.server.repository.ProjectRepository
+import org.opendc.web.server.repository.TopologyRepository
+import java.time.Instant
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt
new file mode 100644
index 00000000..bd14950c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.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.server.service
+
+import org.opendc.web.proto.Trace
+import org.opendc.web.server.repository.TraceRepository
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+/**
+ * Service for managing [Trace]s.
+ */
+@ApplicationScoped
+class TraceService @Inject constructor(private val repository: TraceRepository) {
+ /**
+ * Obtain all available workload traces.
+ */
+ fun findAll(): List<Trace> {
+ return repository.findAll().map { it.toUserDto() }
+ }
+
+ /**
+ * Obtain a workload trace by identifier.
+ */
+ fun findById(id: String): Trace? {
+ return repository.findOne(id)?.toUserDto()
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt
new file mode 100644
index 00000000..ee78d103
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.service
+
+import org.opendc.web.proto.user.Project
+import org.opendc.web.server.model.*
+
+/**
+ * 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-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt
new file mode 100644
index 00000000..2d0da3b3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt
new file mode 100644
index 00000000..0bdf959a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.server.util
+
+import io.quarkus.arc.properties.IfBuildProperty
+import java.security.Principal
+import javax.ws.rs.container.ContainerRequestContext
+import javax.ws.rs.container.ContainerRequestFilter
+import javax.ws.rs.container.PreMatching
+import javax.ws.rs.core.SecurityContext
+import javax.ws.rs.ext.Provider
+
+/**
+ * Helper class to disable security for the OpenDC web API when in development mode.
+ */
+@Provider
+@PreMatching
+@IfBuildProperty(name = "opendc.security.enabled", stringValue = "false")
+class DevSecurityOverrideFilter : ContainerRequestFilter {
+ override fun filter(requestContext: ContainerRequestContext) {
+ requestContext.securityContext = object : SecurityContext {
+ override fun getUserPrincipal(): Principal = Principal { "anon" }
+
+ override fun isSecure(): Boolean = false
+
+ override fun isUserInRole(role: String): Boolean = true
+
+ override fun getAuthenticationScheme(): String = "basic"
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt
new file mode 100644
index 00000000..8634c8a4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..9e29b734
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
new file mode 100644
index 00000000..45752d4e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
@@ -0,0 +1,26 @@
+package org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
new file mode 100644
index 00000000..216c465f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..f5069c4c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
new file mode 100644
index 00000000..3d10cb0e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
@@ -0,0 +1,38 @@
+package org.opendc.web.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt
new file mode 100644
index 00000000..98663640
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt
new file mode 100644
index 00000000..6c6078dd
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/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.server.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-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
new file mode 100644
index 00000000..4704c5ae
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.server.util.runner
+
+import org.opendc.web.proto.JobState
+import org.opendc.web.proto.runner.Job
+import org.opendc.web.runner.JobManager
+import org.opendc.web.server.service.JobService
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.transaction.Transactional
+
+/**
+ * Implementation of [JobManager] that interfaces directly with [JobService] without overhead of the REST API.
+ */
+@ApplicationScoped
+class QuarkusJobManager @Inject constructor(private val service: JobService) : JobManager {
+ @Transactional
+ override fun findNext(): Job? {
+ return service.queryPending().firstOrNull()
+ }
+
+ @Transactional
+ override fun claim(id: Long): Boolean {
+ return try {
+ service.updateState(id, JobState.CLAIMED, null)
+ true
+ } catch (e: IllegalStateException) {
+ false
+ }
+ }
+
+ @Transactional
+ override fun heartbeat(id: Long) {
+ service.updateState(id, JobState.RUNNING, null)
+ }
+
+ @Transactional
+ override fun fail(id: Long) {
+ service.updateState(id, JobState.FAILED, null)
+ }
+
+ @Transactional
+ override fun finish(id: Long, results: Map<String, Any>) {
+ service.updateState(id, JobState.FINISHED, results)
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/resources/META-INF/branding/logo.png b/opendc-web/opendc-web-server/src/main/resources/META-INF/branding/logo.png
new file mode 100644
index 00000000..d743038b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/META-INF/branding/logo.png
Binary files differ
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-dev.properties b/opendc-web/opendc-web-server/src/main/resources/application-dev.properties
new file mode 100644
index 00000000..4065f55f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/application-dev.properties
@@ -0,0 +1,37 @@
+# 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 json;
+
+# Hibernate
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.flyway.clean-at-start=true
+
+# Disable authentication
+opendc.security.enabled=false
+
+# Mount web UI at root and API at "/api"
+quarkus.opendc-ui.path=/
+quarkus.resteasy.path=/api
+
+# Swagger UI
+quarkus.smallrye-openapi.servers=http://localhost:8080
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-docker.properties b/opendc-web/opendc-web-server/src/main/resources/application-docker.properties
new file mode 100644
index 00000000..eae9ee1e
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/application-docker.properties
@@ -0,0 +1,49 @@
+# 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.
+
+# Configuration for standalone Docker server distribution without web UI.
+
+# 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
+
+# Disable OpenDC web UI
+quarkus.opendc-ui.include=false
+
+# Security
+opendc.security.enabled=true
+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
+
+# Swagger UI
+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/v2/}"}
+
+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=https://api.opendc.org
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-prod.properties b/opendc-web/opendc-web-server/src/main/resources/application-prod.properties
new file mode 100644
index 00000000..8e6a9720
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/application-prod.properties
@@ -0,0 +1,37 @@
+# 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:file:./data/opendc;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS json;
+
+# Hibernate
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+
+# Disable authentication
+opendc.security.enabled=false
+quarkus.oidc.enabled=${opendc.security.enabled}
+
+# Mount web UI at root and API at "/api"
+quarkus.opendc-ui.path=/
+quarkus.resteasy.path=/api
+
+# Swagger UI
+quarkus.smallrye-openapi.servers=http://localhost:8080
diff --git a/opendc-web/opendc-web-server/src/main/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties
new file mode 100644
index 00000000..338a00b9
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/application-test.properties
@@ -0,0 +1,37 @@
+# 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 IF NOT EXISTS "JSONB" AS json;
+
+quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
+quarkus.flyway.clean-at-start=true
+
+# Disable security
+quarkus.oidc.enabled=false
+
+# Disable OpenAPI/Swagger
+quarkus.smallrye-openapi.enable=false
+quarkus.swagger-ui.enable=false
+
+# Disable OpenDC web UI and runner
+quarkus.opendc-ui.include=false
+quarkus.opendc-runner.include=false
diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties
new file mode 100644
index 00000000..40933304
--- /dev/null
+++ b/opendc-web/opendc-web-server/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.
+
+# Enable CORS
+quarkus.http.cors=true
+
+# Security
+quarkus.oidc.enabled=${opendc.security.enabled}
+
+# Runner logging
+quarkus.log.category."org.opendc".level=ERROR
+quarkus.log.category."org.opendc.web".level=INFO
+quarkus.log.category."org.apache".level=WARN
+
+# 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
+
+# Flyway database migrations
+quarkus.flyway.baseline-on-migrate=true
+quarkus.flyway.migrate-at-start=true
diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
new file mode 100644
index 00000000..183a70ea
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
@@ -0,0 +1,144 @@
+-- Hibernate sequence for unique identifiers
+create sequence hibernate_sequence start with 1 increment by 1;
+
+-- Projects
+create table projects
+(
+ id bigint not null,
+ created_at timestamp not null,
+ name varchar(255) not null,
+ portfolios_created integer not null,
+ scenarios_created integer not null,
+ topologies_created integer not null,
+ updated_at timestamp not null,
+ primary key (id)
+);
+
+-- Project authorizations authorize users specific permissions to a project.
+create table project_authorizations
+(
+ project_id bigint not null,
+ user_id varchar(255) not null,
+ role integer not null,
+ primary key (project_id, user_id)
+);
+
+-- Topologies represent the datacenter designs created by users.
+create table topologies
+(
+ id bigint not null,
+ created_at timestamp not null,
+ name varchar(255) not null,
+ number integer not null,
+ rooms jsonb not null,
+ updated_at timestamp not null,
+ project_id bigint not null,
+ primary key (id)
+);
+
+-- Portfolios
+create table portfolios
+(
+ id bigint not null,
+ name varchar(255) not null,
+ number integer not null,
+ targets jsonb not null,
+ project_id bigint not null,
+ primary key (id)
+);
+
+create table scenarios
+(
+ id bigint not null,
+ name varchar(255) not null,
+ number integer not null,
+ phenomena jsonb not null,
+ scheduler_name varchar(255) not null,
+ sampling_fraction double precision not null,
+ job_id bigint,
+ portfolio_id bigint not null,
+ project_id bigint not null,
+ topology_id bigint not null,
+ trace_id varchar(255) not null,
+ primary key (id)
+);
+
+create table jobs
+(
+ id bigint not null,
+ created_at timestamp not null,
+ repeats integer not null,
+ results jsonb,
+ state integer not null,
+ updated_at timestamp not null,
+ primary key (id)
+);
+
+-- Workload traces available to the user.
+create table traces
+(
+ id varchar(255) not null,
+ name varchar(255) not null,
+ type varchar(255) not null,
+ primary key (id)
+);
+
+-- Relations
+alter table project_authorizations
+ add constraint FK824hw0npe6gwiamwb6vohsu19
+ foreign key (project_id)
+ references projects;
+
+create index fn_topologies_number on topologies (project_id, number);
+
+alter table topologies
+ add constraint UK2s5na63qtu2of4g7odocmwi2a unique (project_id, number);
+
+alter table topologies
+ add constraint FK1kpw87pylq7m2ct9lq0ed1u3b
+ foreign key (project_id)
+ references projects;
+
+create index fn_portfolios_number on portfolios (project_id, number);
+
+alter table portfolios
+ add constraint FK31ytuaxb7aboxueng9hq7owwa
+ foreign key (project_id)
+ references projects;
+
+alter table portfolios
+ add constraint UK56dtskxruwj22dvxny2hfhks1 unique (project_id, number);
+
+create index fn_scenarios_number on scenarios (project_id, number);
+
+alter table scenarios
+ add constraint UKd0bk6fmtw5qiu9ty7t3g9crqd unique (project_id, number);
+
+alter table scenarios
+ add constraint FK9utvg0i5uu8db9pa17a1d77iy
+ foreign key (job_id)
+ references jobs;
+
+alter table scenarios
+ add constraint FK181y5hv0uibhj7fpbpkdy90s5
+ foreign key (portfolio_id)
+ references portfolios;
+
+alter table scenarios
+ add constraint FKbvwyh4joavs444rj270o3b8fr
+ foreign key (project_id)
+ references projects;
+
+alter table scenarios
+ add constraint FKrk6ltvaf9lp0aukp9dq3qjujj
+ foreign key (topology_id)
+ references topologies;
+
+alter table scenarios
+ add constraint FK5m05tqeekqjkbbsaj3ehl6o8n
+ foreign key (trace_id)
+ references traces;
+
+-- Initial data
+insert into traces (id, name, type)
+values ('bitbrains-small', 'Bitbrains Small', 'vm');