summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server/src/main
diff options
context:
space:
mode:
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');