From e64487cb57ca75d17fe5a8a664c1e8247c7b5168 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 7 Jan 2023 19:23:11 +0000 Subject: refactor(web/server): Use Panache for entity modeling This change updates the OpenDC web server to use Panache (provided by Quarkus) to model entities. Such approach is better supported in Quarkus and simplifies our implementation. --- opendc-web/opendc-web-server/build.gradle.kts | 4 +- .../main/java/org/opendc/web/server/model/Job.java | 142 +++++++++++++ .../org/opendc/web/server/model/Portfolio.java | 135 +++++++++++++ .../java/org/opendc/web/server/model/Project.java | 224 +++++++++++++++++++++ .../web/server/model/ProjectAuthorization.java | 163 +++++++++++++++ .../java/org/opendc/web/server/model/Scenario.java | 189 +++++++++++++++++ .../java/org/opendc/web/server/model/Topology.java | 135 +++++++++++++ .../java/org/opendc/web/server/model/Trace.java | 70 +++++++ .../opendc/web/server/model/UserAccounting.java | 167 +++++++++++++++ .../java/org/opendc/web/server/model/Workload.java | 61 ++++++ .../org/opendc/web/server/service/JobService.java | 138 +++++++++++++ .../web/server/service/PortfolioService.java | 148 ++++++++++++++ .../opendc/web/server/service/ProjectService.java | 106 ++++++++++ .../opendc/web/server/service/ScenarioService.java | 224 +++++++++++++++++++++ .../opendc/web/server/service/TopologyService.java | 178 ++++++++++++++++ .../opendc/web/server/service/TraceService.java | 55 +++++ .../web/server/service/UserAccountingService.java | 136 +++++++++++++ .../org/opendc/web/server/service/UserService.java | 58 ++++++ .../web/server/util/runner/QuarkusJobManager.java | 2 +- .../main/kotlin/org/opendc/web/server/model/Job.kt | 111 ---------- .../org/opendc/web/server/model/Portfolio.kt | 100 --------- .../kotlin/org/opendc/web/server/model/Project.kt | 144 ------------- .../web/server/model/ProjectAuthorization.kt | 64 ------ .../web/server/model/ProjectAuthorizationKey.kt | 38 ---- .../kotlin/org/opendc/web/server/model/Scenario.kt | 118 ----------- .../kotlin/org/opendc/web/server/model/Topology.kt | 100 --------- .../kotlin/org/opendc/web/server/model/Trace.kt | 63 ------ .../org/opendc/web/server/model/UserAccounting.kt | 81 -------- .../kotlin/org/opendc/web/server/model/Workload.kt | 39 ---- .../opendc/web/server/repository/JobRepository.kt | 94 --------- .../web/server/repository/PortfolioRepository.kt | 76 ------- .../web/server/repository/ProjectRepository.kt | 157 --------------- .../web/server/repository/ScenarioRepository.kt | 90 --------- .../web/server/repository/TopologyRepository.kt | 86 -------- .../web/server/repository/TraceRepository.kt | 53 ----- .../server/repository/UserAccountingRepository.kt | 88 -------- .../opendc/web/server/rest/runner/JobResource.kt | 2 +- .../web/server/rest/user/PortfolioResource.kt | 4 +- .../opendc/web/server/rest/user/ProjectResource.kt | 8 +- .../org/opendc/web/server/service/JobService.kt | 97 --------- .../opendc/web/server/service/PortfolioService.kt | 103 ---------- .../opendc/web/server/service/ProjectService.kt | 88 -------- .../opendc/web/server/service/RunnerConversions.kt | 69 ------- .../opendc/web/server/service/ScenarioService.kt | 141 ------------- .../opendc/web/server/service/TopologyService.kt | 127 ------------ .../org/opendc/web/server/service/TraceService.kt | 48 ----- .../web/server/service/UserAccountingService.kt | 128 ------------ .../opendc/web/server/service/UserConversions.kt | 126 ------------ .../org/opendc/web/server/service/UserService.kt | 44 ---- .../kotlin/org/opendc/web/server/service/Utils.kt | 40 ---- .../server/service/UserAccountingServiceTest.java | 213 ++++++++++++++++++++ .../web/server/rest/runner/JobResourceTest.kt | 2 +- .../web/server/rest/user/PortfolioResourceTest.kt | 6 +- .../web/server/rest/user/ProjectResourceTest.kt | 14 +- .../server/service/UserAccountingServiceTest.kt | 203 ------------------- 55 files changed, 2564 insertions(+), 2736 deletions(-) create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt delete mode 100644 opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt create mode 100644 opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java delete mode 100644 opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt (limited to 'opendc-web/opendc-web-server') diff --git a/opendc-web/opendc-web-server/build.gradle.kts b/opendc-web/opendc-web-server/build.gradle.kts index 62c13591..bbf713cd 100644 --- a/opendc-web/opendc-web-server/build.gradle.kts +++ b/opendc-web/opendc-web-server/build.gradle.kts @@ -46,7 +46,8 @@ dependencies { implementation(libs.quarkus.security) implementation(libs.quarkus.oidc) - implementation(libs.quarkus.hibernate.orm) + implementation(libs.quarkus.hibernate.orm.core) + implementation(libs.quarkus.hibernate.orm.panache) implementation(libs.quarkus.hibernate.validator) implementation(libs.quarkus.flyway) implementation(libs.quarkus.jdbc.postgresql) @@ -56,6 +57,7 @@ dependencies { testImplementation(libs.quarkus.junit5.core) testImplementation(libs.quarkus.junit5.mockk) testImplementation(libs.quarkus.jacoco) + testImplementation(libs.quarkus.panache.mock) testImplementation(libs.restassured.core) testImplementation(libs.restassured.kotlin) testImplementation(libs.quarkus.test.security) diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java new file mode 100644 index 00000000..14fd3e2a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -0,0 +1,142 @@ +/* + * 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 io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.JobState; + +/** + * A simulation job to be run by the simulator. + */ +@Entity +@Table(name = "jobs") +@NamedQueries({ + @NamedQuery( + name = "Job.updateOne", + query = + """ + UPDATE Job j + SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results + WHERE j.id = :id AND j.state = :oldState + """) +}) +public class Job extends PanacheEntity { + @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", nullable = false) + public Scenario scenario; + + @Column(name = "created_by", nullable = false, updatable = false) + public String createdBy; + + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The number of simulation runs to perform. + */ + @Column(nullable = false, updatable = false) + public int repeats; + + /** + * The instant at which the job was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt = createdAt; + + /** + * The state of the job. + */ + @Column(nullable = false) + public JobState state = JobState.PENDING; + + /** + * The runtime of the job (in seconds). + */ + @Column(nullable = false) + public int runtime = 0; + + /** + * Experiment results in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb") + public Map results = null; + + /** + * Construct a {@link Job} instance. + */ + public Job(Scenario scenario, String createdBy, Instant createdAt, int repeats) { + this.createdBy = createdBy; + this.scenario = scenario; + this.createdAt = createdAt; + this.repeats = repeats; + } + + /** + * JPA constructor + */ + protected Job() {} + + /** + * Find {@link Job}s in the specified {@link JobState}. + * + * @param state The state of the jobs to find. + * @return The list of jobs that are in the specified state. + */ + public static List findByState(JobState state) { + return find("state", state).list(); + } + + /** + * Atomically update this job. + * + * @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. + */ + public boolean updateAtomically(JobState newState, Instant time, int runtime, Map results) { + long count = update( + "#Job.updateOne", + Parameters.with("id", id) + .and("oldState", state) + .and("newState", newState) + .and("updatedAt", time) + .and("runtime", runtime) + .and("results", results)); + return count > 0; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java new file mode 100644 index 00000000..4c3af570 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 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 io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Targets; + +/** + * A portfolio is the composition of multiple scenarios. + */ +@Entity +@Table( + name = "portfolios", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Portfolio.findByProject", query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"), + @NamedQuery( + name = "Portfolio.findOneByProject", + query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number") +}) +public class Portfolio extends PanacheEntity { + /** + * The {@link Project} this portfolio belongs to. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the portfolio for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of this portfolio. + */ + @Column(nullable = false) + public String name; + + /** + * The portfolio targets (metrics, repetitions). + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public Targets targets; + + /** + * The scenarios in this portfolio. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "portfolio", + orphanRemoval = true) + @OrderBy("id ASC") + public Set scenarios = new HashSet<>(); + + /** + * Construct a {@link Portfolio} object. + */ + public Portfolio(Project project, int number, String name, Targets targets) { + this.project = project; + this.number = number; + this.name = name; + this.targets = targets; + } + + /** + * JPA constructor + */ + protected Portfolio() {} + + /** + * Find all {@link Portfolio}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The list of portfolios that belong to the specified project. + */ + public static List findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * Find the {@link Portfolio} with the specified number belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The portfolio or null if it does not exist. + */ + public static Portfolio findByProject(long projectId, int number) { + return find( + "#Portfolio.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java new file mode 100644 index 00000000..5836e33f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java @@ -0,0 +1,224 @@ +/* + * 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 io.quarkus.hibernate.orm.panache.Panache; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +/** + * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users. + */ +@Entity +@Table(name = "projects") +@NamedQueries({ + @NamedQuery( + name = "Project.findByUser", + 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 + """) +}) +public class Project extends PanacheEntity { + /** + * The name of the project. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the project was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the project was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * The portfolios belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set portfolios = new HashSet<>(); + + /** + * The number of portfolios created for this project (including deleted portfolios). + */ + @Column(name = "portfolios_created", nullable = false) + public int portfoliosCreated = 0; + + /** + * The topologies belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set topologies = new HashSet<>(); + + /** + * The number of topologies created for this project (including deleted topologies). + */ + @Column(name = "topologies_created", nullable = false) + public int topologiesCreated = 0; + + /** + * The scenarios belonging to this project. + */ + @OneToMany(mappedBy = "project", orphanRemoval = true) + public Set scenarios = new HashSet<>(); + + /** + * The number of scenarios created for this project (including deleted scenarios). + */ + @Column(name = "scenarios_created", nullable = false) + public int scenariosCreated = 0; + + /** + * The users authorized to access the project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + public Set authorizations = new HashSet<>(); + + /** + * Construct a {@link Project} object. + */ + public Project(String name, Instant createdAt) { + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } + + /** + * JPA constructor + */ + protected Project() {} + + /** + * Allocate the next portfolio number for the specified [project]. + * + * @param time The time at which the new portfolio is created. + */ + public int allocatePortfolio(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocatePortfolio", + Parameters.with("id", id).and("oldState", portfoliosCreated).and("now", time)); + if (count > 0) { + return portfoliosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next portfolio"); + } + + /** + * Allocate the next topology number for the specified [project]. + * + * @param time The time at which the new topology is created. + */ + public int allocateTopology(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateTopology", + Parameters.with("id", id).and("oldState", topologiesCreated).and("now", time)); + if (count > 0) { + return topologiesCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next topology"); + } + + /** + * Allocate the next scenario number for the specified [project]. + * + * @param time The time at which the new scenario is created. + */ + public int allocateScenario(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateScenario", + Parameters.with("id", id).and("oldState", scenariosCreated).and("now", time)); + if (count > 0) { + return scenariosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next scenario"); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java new file mode 100644 index 00000000..c10fcc64 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -0,0 +1,163 @@ +/* + * 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 io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import org.opendc.web.proto.user.ProjectRole; + +/** + * An authorization for some user to participate in a project. + */ +@Entity +@Table(name = "project_authorizations") +@NamedQueries({ + @NamedQuery( + name = "ProjectAuthorization.findByUser", + query = + """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """), +}) +public class ProjectAuthorization extends PanacheEntityBase { + /** + * The user identifier of the authorization. + */ + @EmbeddedId + public ProjectAuthorization.Key key; + + /** + * 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) + public Project project; + + /** + * The role of the user in the project. + */ + @Column(nullable = false) + public ProjectRole role; + + /** + * Construct a {@link ProjectAuthorization} object. + */ + public ProjectAuthorization(Project project, String userId, ProjectRole role) { + this.key = new ProjectAuthorization.Key(project.id, userId); + this.project = project; + this.role = role; + } + + /** + * JPA constructor + */ + protected ProjectAuthorization() {} + + /** + * 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. + */ + public static List findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)) + .list(); + } + + /** + * 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. + */ + public static ProjectAuthorization findByUser(String userId, long id) { + return findById(new ProjectAuthorization.Key(id, userId)); + } + + /** + * Determine whether the authorization allows the user to edit the project. + */ + public boolean canEdit() { + return switch (role) { + case OWNER, EDITOR -> true; + case VIEWER -> false; + }; + } + + /** + * Determine whether the authorization allows the user to delete the project. + */ + public boolean canDelete() { + return role == ProjectRole.OWNER; + } + + /** + * Key for representing a {@link ProjectAuthorization} object. + */ + @Embeddable + public static class Key implements Serializable { + @Column(name = "project_id", nullable = false) + public long projectId; + + @Column(name = "user_id", nullable = false) + public String userId; + + public Key(long projectId, String userId) { + this.projectId = projectId; + this.userId = userId; + } + + protected Key() {} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key = (Key) o; + return projectId == key.projectId && userId.equals(key.userId); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, userId); + } + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java new file mode 100644 index 00000000..9381f9be --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 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 io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.OperationalPhenomena; + +/** + * A single scenario to be explored by the simulator. + */ +@Entity +@Table( + name = "scenarios", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Scenario.findByProject", query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"), + @NamedQuery( + name = "Scenario.findByPortfolio", + 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.findOneByProject", + query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number") +}) +public class Scenario extends PanacheEntity { + /** + * The {@link Project} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * The {@link Portfolio} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "portfolio_id", nullable = false) + public Portfolio portfolio; + + /** + * Unique number of the scenario for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the scenario. + */ + @Column(nullable = false, updatable = false) + public String name; + + /** + * Workload details of the scenario. + */ + @Embedded + public Workload workload; + + /** + * Topology details of the scenario. + */ + @ManyToOne(optional = false) + public Topology topology; + + /** + * Operational phenomena activated in the scenario. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public OperationalPhenomena phenomena; + + /** + * The name of the VM scheduler used in the scenario. + */ + @Column(name = "scheduler_name", nullable = false, updatable = false) + public String schedulerName; + + /** + * The {@link Job} associated with the scenario. + */ + @OneToOne(cascade = {CascadeType.ALL}) + public Job job; + + /** + * Construct a {@link Scenario} object. + */ + public Scenario( + Project project, + Portfolio portfolio, + int number, + String name, + Workload workload, + Topology topology, + OperationalPhenomena phenomena, + String schedulerName) { + this.project = project; + this.portfolio = portfolio; + this.number = number; + this.name = name; + this.workload = workload; + this.topology = topology; + this.phenomena = phenomena; + this.schedulerName = schedulerName; + } + + /** + * JPA constructor + */ + protected Scenario() {} + + /** + * Find all {@link Scenario}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The list of scenarios that belong to the specified project. + */ + public static List findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * Find all {@link Scenario}s that belong to the specified portfolio. + * + * @param projectId The unique identifier of the project. + * @param number The number of the portfolio. + * @return The list of scenarios that belong to the specified project and portfolio.. + */ + public static List findByPortfolio(long projectId, int number) { + return find( + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)) + .list(); + } + + /** + * Find the {@link Scenario} with the specified number belonging to the specified project. + * + * @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. + */ + public static Scenario findByProject(long projectId, int number) { + return find( + "#Scenario.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java new file mode 100644 index 00000000..6ec83f78 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -0,0 +1,135 @@ +/* + * 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 io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Room; + +/** + * A datacenter design in OpenDC. + */ +@Entity +@Table( + name = "topologies", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), + @NamedQuery( + name = "Topology.findOneByProject", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number") +}) +public class Topology extends PanacheEntity { + /** + * The {@link Project} to which the topology belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the topology for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the topology. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the topology was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the topology was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * Datacenter design in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false) + public List rooms; + + /** + * Construct a {@link Topology} object. + */ + public Topology(Project project, int number, String name, Instant createdAt, List rooms) { + this.project = project; + this.number = number; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + this.rooms = rooms; + } + + /** + * JPA constructor + */ + protected Topology() {} + + /** + * 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. + */ + public static List findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * 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. + */ + public static Topology findByProject(long projectId, int number) { + return find( + "#Topology.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java new file mode 100644 index 00000000..f73c8494 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -0,0 +1,70 @@ +/* + * 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 io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * A workload trace available for simulation. + */ +@Entity +public class Trace extends PanacheEntityBase { + /** + * The unique identifier of the trace. + */ + @Id + public String id; + + /** + * The name of the trace. + */ + @Column(nullable = false, updatable = false) + public String name; + + /** + * The type of trace. + */ + @Column(nullable = false, updatable = false) + public String type; + + /** + * Construct a {@link Trace}. + * + * @param id The unique identifier of the trace. + * @param name The name of the trace. + * @param type The type of trace. + */ + public Trace(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + + /** + * JPA constructor. + */ + protected Trace() {} +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java new file mode 100644 index 00000000..fda4302f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023 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 io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; +import java.time.LocalDate; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +/** + * Entity to track the number of simulation minutes used by a user. + */ +@Entity +@Table(name = "user_accounting") +@NamedQueries({ + @NamedQuery( + name = "UserAccounting.consumeBudget", + query = + """ + UPDATE UserAccounting a + SET a.simulationTime = a.simulationTime + :seconds + WHERE a.userId = :userId AND a.periodEnd = :periodEnd + """), + @NamedQuery( + name = "UserAccounting.resetBudget", + query = + """ + UPDATE UserAccounting a + SET a.periodEnd = :periodEnd, a.simulationTime = :seconds + WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd + """) +}) +public class UserAccounting extends PanacheEntityBase { + /** + * User to which this object belongs. + */ + @Id + @Column(name = "user_id", nullable = false) + public String userId; + + /** + * The end of the accounting period. + */ + @Column(name = "period_end", nullable = false) + public LocalDate periodEnd; + + /** + * The number of simulation seconds to be used per accounting period. + */ + @Column(name = "simulation_time_budget", nullable = false) + public int simulationTimeBudget; + + /** + * The number of simulation seconds used in this period. This number should reset once the accounting period has + * been reached. + */ + @Column(name = "simulation_time", nullable = false) + public int simulationTime = 0; + + /** + * Construct a new {@link UserAccounting} object. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + */ + public UserAccounting(String userId, LocalDate periodEnd, int simulationTimeBudget) { + this.userId = userId; + this.periodEnd = periodEnd; + this.simulationTimeBudget = simulationTimeBudget; + } + + /** + * JPA constructor. + */ + protected UserAccounting() {} + + /** + * Return the {@link UserAccounting} object associated with the specified user id. + */ + public static UserAccounting findByUser(String userId) { + return findById(userId); + } + + /** + * Create a new {@link UserAccounting} object and persist it to the database. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + * @param simulationTime The initial simulation time that has been consumed. + */ + public static UserAccounting create( + String userId, LocalDate periodEnd, int simulationTimeBudget, int simulationTime) { + UserAccounting newAccounting = new UserAccounting(userId, periodEnd, simulationTimeBudget); + newAccounting.simulationTime = simulationTime; + newAccounting.persistAndFlush(); + return newAccounting; + } + + /** + * Atomically consume the budget for this {@link UserAccounting} object. + * + * @param seconds The number of seconds to consume from the user. + * @return true when the update succeeded, false when there was a conflict. + */ + public boolean consumeBudget(int seconds) { + long count = update( + "#UserAccounting.consumeBudget", + Parameters.with("userId", userId).and("periodEnd", periodEnd).and("seconds", seconds)); + return count > 0; + } + + /** + * Atomically reset the budget for this {@link UserAccounting} object. + * + * @param periodEnd The new end period for the budget. + * @param seconds The number of seconds that have already been consumed. + * @return true when the update succeeded`, false when there was a conflict. + */ + public boolean resetBudget(LocalDate periodEnd, int seconds) { + long count = update( + "#UserAccounting.resetBudget", + Parameters.with("userId", userId) + .and("oldPeriodEnd", this.periodEnd) + .and("periodEnd", periodEnd) + .and("seconds", seconds)); + return count > 0; + } + + /** + * Determine whether the user has any remaining simulation budget. + * + * @return true when the user still has budget left, false otherwise. + */ + public boolean hasSimulationBudget() { + var today = LocalDate.now(); + + // The accounting period must be over or there must be budget remaining. + return !today.isBefore(periodEnd) || simulationTimeBudget > simulationTime; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java new file mode 100644 index 00000000..129fb0c5 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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 {@link Scenario} + */ +@Embeddable +public class Workload { + /** + * The {@link Trace} that the workload runs. + */ + @ManyToOne(optional = false) + public Trace trace; + + /** + * The percentage of the trace that should be sampled. + */ + @Column(name = "sampling_fraction", nullable = false, updatable = false) + public double samplingFraction; + + /** + * Construct a {@link Workload} object. + * + * @param trace The {@link Trace} to run as workload. + * @param samplingFraction The percentage of the workload to sample. + */ + public Workload(Trace trace, double samplingFraction) { + this.trace = trace; + this.samplingFraction = samplingFraction; + } + + /** + * JPA constructor. + */ + protected Workload() {} +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java new file mode 100644 index 00000000..eb0982ec --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 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 java.time.Instant; +import java.util.List; +import java.util.Map; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * Service for managing {@link Job}s. + */ +@ApplicationScoped +public final class JobService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link JobService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public JobService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Query the pending simulation jobs. + */ + public List listPending() { + return Job.findByState(JobState.PENDING).stream() + .map(JobService::toRunnerDto) + .toList(); + } + + /** + * Find a job by its identifier. + */ + public org.opendc.web.proto.runner.Job findById(long id) { + return toRunnerDto(Job.findById(id)); + } + + /** + * Atomically update the state of a {@link Job}. + * + * @param id The identifier of the job. + * @param newState The next state for the job. + * @param runtime The runtime of the job (in seconds). + * @param results The potential results of the job. + */ + public org.opendc.web.proto.runner.Job updateState( + long id, JobState newState, int runtime, Map results) { + Job entity = Job.findById(id); + if (entity == null) { + return null; + } + + JobState state = entity.state; + if (!isTransitionLegal(state, newState)) { + throw new IllegalArgumentException("Invalid transition from %s to %s".formatted(state, newState)); + } + + Instant now = Instant.now(); + JobState nextState = newState; + int consumedBudget = Math.min(1, runtime - entity.runtime); + + // Check whether the user still has any simulation budget left + if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) + && nextState == JobState.RUNNING) { + nextState = JobState.FAILED; // User has consumed all their budget; cancel the job + } + + if (!entity.updateAtomically(nextState, now, runtime, results)) { + throw new IllegalStateException("Conflicting update"); + } + + return toRunnerDto(entity); + } + + /** + * Determine whether the transition from [this] to [newState] is legal. + */ + public static boolean isTransitionLegal(JobState currentState, JobState newState) { + // Note that we always allow transitions from the state + return newState == currentState + || switch (currentState) { + case PENDING -> newState == JobState.CLAIMED; + case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; + case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; + case FINISHED, FAILED -> false; + }; + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toUserDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, + ScenarioService.toRunnerDto(job.scenario), + job.state, + job.createdAt, + job.updatedAt, + job.runtime, + job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java new file mode 100644 index 00000000..94da5195 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 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 java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * Service for managing {@link Portfolio}s. + */ +@ApplicationScoped +public final class PortfolioService { + /** + * List all {@link Portfolio}s that belong a certain project. + */ + public List findByUser(String userId, long projectId) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).stream() + .map((p) -> toUserDto(p, auth)) + .toList(); + } + + /** + * Find a {@link Portfolio} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + + if (portfolio == null) { + return null; + } + + return toUserDto(portfolio, auth); + } + + /** + * Delete the portfolio with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { + return null; + } + + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Portfolio} with the specified name. + */ + public org.opendc.web.proto.user.Portfolio create( + String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocatePortfolio(now); + + Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); + + project.portfolios.add(portfolio); + portfolio.persist(); + + return toUserDto(portfolio, auth); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + ProjectService.toUserDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(ScenarioService::toSummaryDto).toList()); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. + */ + public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { + return new org.opendc.web.proto.user.Portfolio.Summary( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java new file mode 100644 index 00000000..aeef664e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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 java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * Service for managing {@link Project}s. + */ +@ApplicationScoped +public final class ProjectService { + /** + * List all projects for the user with the specified userId. + */ + public List findByUser(String userId) { + return ProjectAuthorization.findByUser(userId).stream() + .map(ProjectService::toUserDto) + .toList(); + } + + /** + * Obtain the project with the specified id for the user with the specified userId. + */ + public org.opendc.web.proto.user.Project findByUser(String userId, long id) { + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); + + if (auth == null) { + return null; + } + + return toUserDto(auth); + } + + /** + * Create a new {@link Project} for the user with the specified userId. + */ + public org.opendc.web.proto.user.Project create(String userId, String name) { + Instant now = Instant.now(); + Project entity = new Project(name, now); + entity.persist(); + + ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return toUserDto(authorization); + } + + /** + * Delete a project by its identifier. + * + * @param userId The user that invokes the action. + * @param id The identifier of the project. + */ + public org.opendc.web.proto.user.Project delete(String userId, long id) { + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); + + if (auth == null) { + return null; + } + + if (!auth.canDelete()) { + throw new IllegalArgumentException("Not allowed to delete project"); + } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = toUserDto(auth); + auth.project.delete(); + return project; + } + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java new file mode 100644 index 00000000..bf5206af --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023 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 java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; + +/** + * Service for managing {@link Scenario}s. + */ +@ApplicationScoped +public final class ScenarioService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link ScenarioService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public ScenarioService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * List all {@link Scenario}s that belong a certain portfolio. + */ + public List findAll(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Scenario.findByPortfolio(projectId, number).stream() + .map((s) -> toUserDto(s, auth)) + .toList(); + } + + /** + * Obtain a {@link Scenario} by identifier. + */ + public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Scenario scenario = Scenario.findByProject(projectId, number); + + if (scenario == null) { + return null; + } + + return toUserDto(scenario, auth); + } + + /** + * Delete the specified scenario. + */ + public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { + return null; + } + + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Scenario} with the specified data. + */ + public org.opendc.web.proto.user.Scenario create( + String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + return null; + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new IllegalArgumentException("Referred topology does not exist"); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new IllegalArgumentException("Referred trace does not exist"); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocateScenario(now); + + Scenario scenario = new Scenario( + project, + portfolio, + number, + request.getName(), + new Workload(trace, request.getWorkload().getSamplingFraction()), + topology, + request.getPhenomena(), + request.getSchedulerName()); + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.job = job; + portfolio.scenarios.add(scenario); + scenario.persist(); + + return toUserDto(scenario, auth); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + ProjectService.toUserDto(auth), + PortfolioService.toSummaryDto(scenario.portfolio), + scenario.name, + toDto(scenario.workload), + TopologyService.toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + JobService.toUserDto(scenario.job)); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. + */ + public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { + return new org.opendc.web.proto.user.Scenario.Summary( + scenario.id, + scenario.number, + scenario.name, + toDto(scenario.workload), + TopologyService.toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + JobService.toUserDto(scenario.job)); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + PortfolioService.toRunnerDto(scenario.portfolio), + scenario.name, + toDto(scenario.workload), + TopologyService.toRunnerDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Workload} entity into a DTO. + */ + public static org.opendc.web.proto.Workload toDto(Workload workload) { + return new org.opendc.web.proto.Workload(TraceService.toUserDto(workload.trace), workload.samplingFraction); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java new file mode 100644 index 00000000..1961995f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 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 java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; + +/** + * Service for managing {@link Topology}s. + */ +@ApplicationScoped +public final class TopologyService { + /** + * List all {@link Topology}s that belong a certain project. + */ + public List findAll(String userId, long projectId) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).stream() + .map((t) -> toUserDto(t, auth)) + .toList(); + } + + /** + * Find the {@link Topology} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Topology topology = Topology.findByProject(projectId, number); + + if (topology == null) { + return null; + } + + return toUserDto(topology, auth); + } + + /** + * Delete the {@link Topology} with the specified number belonging to projectId + */ + public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + return null; + } + + entity.updatedAt = Instant.now(); + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Update a {@link Topology} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Topology update( + String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + return null; + } + + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Topology} with the specified name. + */ + public org.opendc.web.proto.user.Topology create( + String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Instant now = Instant.now(); + Project project = auth.project; + int number = project.allocateTopology(now); + + Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); + + project.topologies.add(topology); + topology.persist(); + + return toUserDto(topology, auth); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + ProjectService.toUserDto(auth), + topology.name, + topology.rooms, + topology.createdAt, + topology.updatedAt); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. + */ + public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { + return new org.opendc.web.proto.user.Topology.Summary( + topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toRunnerDto(Topology topology) { + return new org.opendc.web.proto.runner.Topology( + topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java new file mode 100644 index 00000000..94b8340b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 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 java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Trace; + +/** + * Service for managing {@link Trace}s. + */ +@ApplicationScoped +public final class TraceService { + /** + * Obtain all available workload traces. + */ + public List findAll() { + List entities = Trace.listAll(); + return entities.stream().map(TraceService::toUserDto).toList(); + } + + /** + * Obtain a workload trace by identifier. + */ + public org.opendc.web.proto.Trace findById(String id) { + return toUserDto(Trace.findById(id)); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toUserDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java new file mode 100644 index 00000000..e5003cb4 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 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 java.time.Duration; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityExistsException; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.opendc.web.server.model.UserAccounting; + +/** + * Service to track the simulation budget of users. + */ +@ApplicationScoped +public final class UserAccountingService { + /** + * The default simulation budget for new users. + */ + private final Duration simulationBudget; + + /** + * Construct a {@link UserAccountingService} instance. + * + * @param simulationBudget The default simulation budget for new users. + */ + public UserAccountingService( + @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m") + Duration simulationBudget) { + this.simulationBudget = simulationBudget; + } + + /** + * Return the {@link org.opendc.web.proto.user.UserAccounting} object for the user with the + * specified userId. If the object does not exist in the database, a default value is constructed. + */ + public org.opendc.web.proto.user.UserAccounting getAccounting(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting != null) { + return new org.opendc.web.proto.user.UserAccounting( + accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget); + } + + return new org.opendc.web.proto.user.UserAccounting( + getNextAccountingPeriod(LocalDate.now()), 0, (int) simulationBudget.toSeconds()); + } + + /** + * Determine whether the user with userId has any remaining simulation budget. + * + * @param userId The unique identifier of the user. + * @return true when the user still has budget left, false otherwise. + */ + public boolean hasSimulationBudget(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting == null) { + return true; + } + return accounting.hasSimulationBudget(); + } + + /** + * Consume seconds from the simulation budget of the user with userId. + * + * @param userId The unique identifier of the user. + * @param seconds The seconds to consume from the simulation budget. + * @return true if the user has consumed his full budget or false if there is still budget + * remaining. + */ + public boolean consumeSimulationBudget(String userId, int seconds) { + LocalDate today = LocalDate.now(); + LocalDate nextAccountingPeriod = getNextAccountingPeriod(today); + + // We need to be careful to prevent conflicts in case of concurrency + // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance + // creates the object concurrently. + // 2. Second, we check if the budget needs to be reset and try this atomically. + // 3. Finally, we atomically consume the budget from the object + // This is repeated three times in case there is a conflict + for (int i = 0; i < 3; i++) { + UserAccounting accounting = UserAccounting.findByUser(userId); + + if (accounting == null) { + try { + UserAccounting newAccounting = UserAccounting.create( + userId, nextAccountingPeriod, (int) simulationBudget.toSeconds(), seconds); + return !newAccounting.hasSimulationBudget(); + } catch (EntityExistsException e) { + // Conflict due to concurrency; retry + } + } else { + boolean success; + + if (!today.isBefore(accounting.periodEnd)) { + success = accounting.resetBudget(nextAccountingPeriod, seconds); + } else { + success = accounting.consumeBudget(seconds); + } + + if (success) { + return !accounting.hasSimulationBudget(); + } + } + } + + throw new IllegalStateException("Failed to allocate consume budget due to conflict"); + } + + /** + * Helper method to find next accounting period. + */ + private static LocalDate getNextAccountingPeriod(LocalDate today) { + return today.with(TemporalAdjusters.firstDayOfNextMonth()); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java new file mode 100644 index 00000000..b46b799b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 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 io.quarkus.security.identity.SecurityIdentity; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.user.User; +import org.opendc.web.proto.user.UserAccounting; + +/** + * Service for managing {@link User}s. + */ +@ApplicationScoped +public final class UserService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link UserService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public UserService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Obtain the {@link User} object for the specified identity. + */ + public User getUser(SecurityIdentity identity) { + String userId = identity.getPrincipal().getName(); + UserAccounting accounting = accountingService.getAccounting(userId); + + return new User(userId, accounting); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java index ed1f7bf1..84ebd6e4 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -47,7 +47,7 @@ public class QuarkusJobManager implements JobManager { @Nullable @Override public Job findNext() { - var pending = jobService.queryPending(); + var pending = jobService.listPending(); if (pending.isEmpty()) { return null; } 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 deleted file mode 100644 index 9c260fc1..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.opendc.web.proto.JobState -import java.time.Instant -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.FetchType -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.JoinColumn -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToOne -import javax.persistence.Table - -/** - * A simulation job to be run by the simulator. - */ -@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.runtime = :runtime, j.results = :results - WHERE j.id = :id AND j.state = :oldState - """ - ) - ] -) -class Job( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - @Column(name = "created_by", nullable = false, updatable = false) - val createdBy: String, - - @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 - - /** - * The runtime of the job (in seconds). - */ - @Column(nullable = false) - var runtime: Int = 0 - - /** - * Experiment results in JSON - */ - @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") - @Column(columnDefinition = "jsonb") - var results: Map? = 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 deleted file mode 100644 index edf1205f..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.opendc.web.proto.Targets -import javax.persistence.CascadeType -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToMany -import javax.persistence.OrderBy -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A portfolio is the composition of multiple scenarios. - */ -@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 = "io.hypersistence.utils.hibernate.type.json.JsonType") - @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 = 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 deleted file mode 100644 index 41d1a786..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.CascadeType -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToMany -import javax.persistence.OrderBy -import javax.persistence.Table - -/** - * 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 = 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 = 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 = 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 = 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 deleted file mode 100644 index 791725cd..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.Column -import javax.persistence.EmbeddedId -import javax.persistence.Entity -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.MapsId -import javax.persistence.Table - -/** - * 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 deleted file mode 100644 index 449b6608..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 deleted file mode 100644 index 47c3e8b2..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.opendc.web.proto.OperationalPhenomena -import javax.persistence.CascadeType -import javax.persistence.Column -import javax.persistence.Embedded -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToOne -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A single scenario to be explored by the simulator. - */ -@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 = "io.hypersistence.utils.hibernate.type.json.JsonType") - @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 deleted file mode 100644 index fe48a0f2..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.opendc.web.proto.Room -import java.time.Instant -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A datacenter design in OpenDC. - */ -@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 = "io.hypersistence.utils.hibernate.type.json.JsonType") - @Column(columnDefinition = "jsonb", nullable = false) - var rooms: List = 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 deleted file mode 100644 index 14a88c5a..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2021 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.model - -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table - -/** - * 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/UserAccounting.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt deleted file mode 100644 index 5b813044..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.LocalDate -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table - -/** - * Entity to track the number of simulation minutes used by a user. - */ -@Entity -@Table(name = "user_accounting") -@NamedQueries( - value = [ - NamedQuery( - name = "UserAccounting.consumeBudget", - query = """ - UPDATE UserAccounting a - SET a.simulationTime = a.simulationTime + :seconds - WHERE a.userId = :userId AND a.periodEnd = :periodEnd - """ - ), - NamedQuery( - name = "UserAccounting.resetBudget", - query = """ - UPDATE UserAccounting a - SET a.periodEnd = :periodEnd, a.simulationTime = :seconds - WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd - """ - ) - ] -) -class UserAccounting( - @Id - @Column(name = "user_id", nullable = false) - val userId: String, - - /** - * The end of the accounting period. - */ - @Column(name = "period_end", nullable = false) - var periodEnd: LocalDate, - - /** - * The number of simulation seconds to be used per accounting period. - */ - @Column(name = "simulation_time_budget", nullable = false) - var simulationTimeBudget: Int -) { - /** - * The number of simulation seconds used in this period. This number should reset once the accounting period has - * been reached. - */ - @Column(name = "simulation_time", nullable = false) - var simulationTime: Int = 0 -} 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 deleted file mode 100644 index 9c59dc25..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 deleted file mode 100644 index e9bf0af0..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 { - 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, runtime: Int, results: Map?): Boolean { - val count = em.createNamedQuery("Job.updateOne") - .setParameter("id", job.id) - .setParameter("oldState", job.state) - .setParameter("newState", newState) - .setParameter("updatedAt", Instant.now()) - .setParameter("runtime", runtime) - .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 deleted file mode 100644 index 77130c15..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 { - 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 deleted file mode 100644 index 519da3de..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 { - 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 deleted file mode 100644 index 145db71d..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 { - 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 { - 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 deleted file mode 100644 index e8eadd63..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 { - 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 deleted file mode 100644 index f328eea6..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { - 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/repository/UserAccountingRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt deleted file mode 100644 index f0265d3d..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.UserAccounting -import java.time.LocalDate -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [UserAccounting] entities. - */ -@ApplicationScoped -class UserAccountingRepository @Inject constructor(private val em: EntityManager) { - /** - * Find the [UserAccounting] object for the specified [userId]. - * - * @param userId The unique identifier of the user. - * @return The [UserAccounting] object or `null` if it does not exist. - */ - fun findForUser(userId: String): UserAccounting? { - return em.find(UserAccounting::class.java, userId) - } - - /** - * Save the specified [UserAccounting] object to the database. - */ - fun save(accounting: UserAccounting) { - em.persist(accounting) - } - - /** - * Atomically consume the budget for the specified [UserAccounting] object. - * - * @param accounting The [UserAccounting] object to update atomically. - * @param seconds The number of seconds to consume from the user. - * @return `true` when the update succeeded`, `false` when there was a conflict. - */ - fun consumeBudget(accounting: UserAccounting, seconds: Int): Boolean { - val count = em.createNamedQuery("UserAccounting.consumeBudget") - .setParameter("userId", accounting.userId) - .setParameter("periodEnd", accounting.periodEnd) - .setParameter("seconds", seconds) - .executeUpdate() - em.refresh(accounting) - return count > 0 - } - - /** - * Atomically reset the budget for the specified [UserAccounting] object. - * - * @param accounting The [UserAccounting] object to update atomically. - * @param periodEnd The new end period for the budget. - * @param seconds The number of seconds that have already been consumed. - * @return `true` when the update succeeded`, `false` when there was a conflict. - */ - fun resetBudget(accounting: UserAccounting, periodEnd: LocalDate, seconds: Int): Boolean { - val count = em.createNamedQuery("UserAccounting.resetBudget") - .setParameter("userId", accounting.userId) - .setParameter("oldPeriodEnd", accounting.periodEnd) - .setParameter("periodEnd", periodEnd) - .setParameter("seconds", seconds) - .executeUpdate() - em.refresh(accounting) - return count > 0 - } -} 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 index d0432360..1e9abc14 100644 --- 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 @@ -45,7 +45,7 @@ class JobResource @Inject constructor(private val jobService: JobService) { */ @GET fun queryPending(): List { - return jobService.queryPending() + return jobService.listPending() } /** 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 index ebe57ae2..82843a5a 100644 --- 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 @@ -50,7 +50,7 @@ class PortfolioResource @Inject constructor( */ @GET fun getAll(@PathParam("project") projectId: Long): List { - return portfolioService.findAll(identity.principal.name, projectId) + return portfolioService.findByUser(identity.principal.name, projectId) } /** @@ -68,7 +68,7 @@ class PortfolioResource @Inject constructor( @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) + return portfolioService.findByUser(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/ProjectResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt index 817f53a5..d12fc690 100644 --- 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 @@ -50,7 +50,7 @@ class ProjectResource @Inject constructor( */ @GET fun getAll(): List { - return projectService.findWithUser(identity.principal.name) + return projectService.findByUser(identity.principal.name) } /** @@ -59,7 +59,7 @@ class ProjectResource @Inject constructor( @POST @Transactional fun create(@Valid request: Project.Create): Project { - return projectService.createForUser(identity.principal.name, request.name) + return projectService.create(identity.principal.name, request.name) } /** @@ -68,7 +68,7 @@ class ProjectResource @Inject constructor( @GET @Path("{project}") fun get(@PathParam("project") id: Long): Project { - return projectService.findWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) + return projectService.findByUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) } /** @@ -79,7 +79,7 @@ class ProjectResource @Inject constructor( @Transactional fun delete(@PathParam("project") id: Long): Project { try { - return projectService.deleteWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) + return projectService.delete(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/service/JobService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt deleted file mode 100644 index a0ebd4f4..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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, - private val accountingService: UserAccountingService -) { - /** - * Query the pending simulation jobs. - */ - fun queryPending(): List { - 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]. - * - * @param id The identifier of the job. - * @param newState The next state for the job. - * @param runtime The runtime of the job (in seconds). - * @param results The potential results of the job. - */ - fun updateState(id: Long, newState: JobState, runtime: Int, results: Map?): 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() - var nextState = newState - val consumedBudget = (runtime - entity.runtime).coerceAtLeast(1) - - // Check whether the user still has any simulation budget left - if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) && nextState == JobState.RUNNING) { - nextState = JobState.FAILED // User has consumed all their budget; cancel the job - } - - if (!repository.updateOne(entity, nextState, now, runtime, 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 deleted file mode 100644 index c83b7a54..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.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 { - // 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 deleted file mode 100644 index 2fc5a054..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 -import org.opendc.web.server.model.Project -import org.opendc.web.server.model.ProjectAuthorization -import org.opendc.web.server.model.ProjectAuthorizationKey -import org.opendc.web.server.repository.ProjectRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.proto.user.Project as ProjectDto - -/** - * 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 { - 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): ProjectDto? { - return repository.findOne(userId, id)?.toUserDto() - } - - /** - * Create a new [Project] for the user with the specified [userId]. - */ - fun createForUser(userId: String, name: String): ProjectDto { - 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): ProjectDto? { - 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 deleted file mode 100644 index 465ac2df..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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, runtime, 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 deleted file mode 100644 index 083f2451..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.server.model.Job -import org.opendc.web.server.model.Scenario -import org.opendc.web.server.model.Workload -import org.opendc.web.server.repository.PortfolioRepository -import org.opendc.web.server.repository.ProjectRepository -import org.opendc.web.server.repository.ScenarioRepository -import org.opendc.web.server.repository.TopologyRepository -import org.opendc.web.server.repository.TraceRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.proto.user.Scenario as ScenarioDto - -/** - * 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, - private val accountingService: UserAccountingService -) { - /** - * List all [Scenario]s that belong a certain portfolio. - */ - fun findAll(userId: String, projectId: Long, number: Int): List { - // 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): ScenarioDto? { - // 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): ScenarioDto? { - // 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: ScenarioDto.Create): ScenarioDto? { - // 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, userId, scenario, now, portfolio.targets.repeats) - - // Fail the job if there is not enough budget for the simulation - if (!accountingService.hasSimulationBudget(userId)) { - job.state = JobState.FAILED - } - - 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 deleted file mode 100644 index 5c2a457a..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { - // 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 deleted file mode 100644 index bd14950c..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2021 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.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 { - 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/UserAccountingService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt deleted file mode 100644 index 11066bfb..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.eclipse.microprofile.config.inject.ConfigProperty -import org.opendc.web.server.model.UserAccounting -import org.opendc.web.server.repository.UserAccountingRepository -import java.time.Duration -import java.time.LocalDate -import java.time.temporal.TemporalAdjusters -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityExistsException -import org.opendc.web.proto.user.UserAccounting as UserAccountingDto - -/** - * Service for tracking the simulation budget of users. - * - * @param repository The [UserAccountingRepository] used to communicate with the database. - * @param simulationBudget The default simulation budget for new users. - */ -@ApplicationScoped -class UserAccountingService @Inject constructor( - private val repository: UserAccountingRepository, - @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m") - private val simulationBudget: Duration -) { - /** - * Return the [UserAccountingDto] object for the user with the specified [userId]. If the object does not exist in the - * database, a default value is constructed. - */ - fun getAccounting(userId: String): UserAccountingDto { - val accounting = repository.findForUser(userId) - return if (accounting != null) { - UserAccountingDto(accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget) - } else { - UserAccountingDto(getNextAccountingPeriod(), 0, simulationBudget.toSeconds().toInt()) - } - } - - /** - * Determine whether the user with [userId] has any remaining simulation budget. - * - * @param userId The unique identifier of the user. - * @return `true` when the user still has budget left, `false` otherwise. - */ - fun hasSimulationBudget(userId: String): Boolean { - val accounting = repository.findForUser(userId) ?: return true - val today = LocalDate.now() - - // The accounting period must be over or there must be budget remaining. - return !today.isBefore(accounting.periodEnd) || accounting.simulationTimeBudget > accounting.simulationTime - } - - /** - * Consume [seconds] from the simulation budget of the user with [userId]. - * - * @param userId The unique identifier of the user. - * @param seconds The seconds to consume from the simulation budget. - * @param `true` if the user has consumed his full budget or `false` if there is still budget remaining. - */ - fun consumeSimulationBudget(userId: String, seconds: Int): Boolean { - val today = LocalDate.now() - val nextAccountingPeriod = getNextAccountingPeriod(today) - val repository = repository - - // We need to be careful to prevent conflicts in case of concurrency - // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance - // creates the object concurrently. - // 2. Second, we check if the budget needs to be reset and try this atomically. - // 3. Finally, we atomically consume the budget from the object - // This is repeated three times in case there is a conflict - repeat(3) { - val accounting = repository.findForUser(userId) - - if (accounting == null) { - try { - val newAccounting = UserAccounting(userId, nextAccountingPeriod, simulationBudget.toSeconds().toInt()) - newAccounting.simulationTime = seconds - repository.save(newAccounting) - - return newAccounting.simulationTime >= newAccounting.simulationTimeBudget - } catch (e: EntityExistsException) { - // Conflict due to concurrency; retry - } - } else { - val success = if (!today.isBefore(accounting.periodEnd)) { - repository.resetBudget(accounting, nextAccountingPeriod, seconds) - } else { - repository.consumeBudget(accounting, seconds) - } - - if (success) { - return accounting.simulationTimeBudget <= accounting.simulationTime - } - } - } - - throw IllegalStateException("Failed to allocate consume budget due to conflict") - } - - /** - * Helper method to find next accounting period. - */ - private fun getNextAccountingPeriod(today: LocalDate = LocalDate.now()): LocalDate { - return today.with(TemporalAdjusters.firstDayOfNextMonth()) - } -} 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 deleted file mode 100644 index e28d9c0f..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.Job -import org.opendc.web.server.model.Portfolio -import org.opendc.web.server.model.ProjectAuthorization -import org.opendc.web.server.model.Scenario -import org.opendc.web.server.model.Topology -import org.opendc.web.server.model.Trace -import org.opendc.web.server.model.Workload - -/** - * 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/UserService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt deleted file mode 100644 index 39352267..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.User -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject - -/** - * Service for managing [User]s. - */ -@ApplicationScoped -class UserService @Inject constructor(private val accounting: UserAccountingService) { - /** - * Obtain the [User] object for the specified [identity]. - */ - fun getUser(identity: SecurityIdentity): User { - val userId = identity.principal.name - val accounting = accounting.getAccounting(userId) - - return User(userId, accounting) - } -} 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 deleted file mode 100644 index 2d0da3b3..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java new file mode 100644 index 00000000..d1d82097 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 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 static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; + +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.QuarkusTest; +import java.time.Duration; +import java.time.LocalDate; +import javax.persistence.EntityExistsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opendc.web.server.model.UserAccounting; + +/** + * Test suite for the {@link UserAccountingService}. + */ +@QuarkusTest +public class UserAccountingServiceTest { + /** + * The {@link UserAccountingService} instance under test. + */ + private UserAccountingService service; + + /** + * The user id to test with + */ + private final String userId = "test"; + + @BeforeEach + public void setUp() { + PanacheMock.mock(UserAccounting.class); + service = new UserAccountingService(Duration.ofHours(1)); + } + + @Test + public void testGetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + var accounting = service.getAccounting(userId); + + assertTrue(accounting.getPeriodEnd().isAfter(LocalDate.now())); + assertEquals(0, accounting.getSimulationTime()); + } + + @Test + public void testGetUserDoesExist() { + var now = LocalDate.now(); + var periodEnd = now.plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 32; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + var accounting = service.getAccounting(userId); + + assertAll( + () -> assertEquals(periodEnd, accounting.getPeriodEnd()), + () -> assertEquals(32, accounting.getSimulationTime()), + () -> assertEquals(3600, accounting.getSimulationTimeBudget())); + } + + @Test + public void testHasBudgetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudget() { + var periodEnd = LocalDate.now().plusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceededButPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceeded() { + var periodEnd = LocalDate.now().plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertFalse(service.hasSimulationBudget(userId)); + } + + @Test + public void testConsumeBudgetNewUser() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetNewUserExceeded() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testConsumeBudgetNewUserConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null).thenReturn(accountingMock); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenThrow(new EntityExistsException()); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.getArgument(0); + return true; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetResetSuccess() { + var periodEnd = LocalDate.now().minusMonths(2); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + accountingMock.simulationTime = 3900; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.resetBudget(any(), anyInt())).thenAnswer((i) -> { + accountingMock.periodEnd = i.getArgument(0); + accountingMock.simulationTime += i.getArgument(1); + return true; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testInfiniteConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.getArgument(0); + return false; + }); + + assertThrows(IllegalStateException.class, () -> service.consumeSimulationBudget(userId, 10)); + } +} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt index 4a86c928..753b9ac4 100644 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt +++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt @@ -101,7 +101,7 @@ class JobResourceTest { @Test @TestSecurity(user = "testUser", roles = ["runner"]) fun testQuery() { - every { jobService.queryPending() } returns listOf(dummyJob) + every { jobService.listPending() } returns listOf(dummyJob) When { get() diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt index 5798d2e7..3ef63a51 100644 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt +++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt @@ -68,7 +68,7 @@ class PortfolioResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetForProject() { - every { portfolioService.findAll("testUser", 1) } returns emptyList() + every { portfolioService.findByUser("testUser", 1) } returns emptyList() Given { pathParam("project", "1") @@ -197,7 +197,7 @@ class PortfolioResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetNonExisting() { - every { portfolioService.findOne("testUser", 1, 1) } returns null + every { portfolioService.findByUser("testUser", 1, 1) } returns null Given { pathParam("project", "1") @@ -215,7 +215,7 @@ class PortfolioResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetExisting() { - every { portfolioService.findOne("testUser", 1, 1) } returns dummyPortfolio + every { portfolioService.findByUser("testUser", 1, 1) } returns dummyPortfolio Given { pathParam("project", "1") diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt index fec8759c..0be56c56 100644 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt +++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt @@ -91,7 +91,7 @@ class ProjectResourceTest { @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetAll() { val projects = listOf(dummyProject) - every { projectService.findWithUser("testUser") } returns projects + every { projectService.findByUser("testUser") } returns projects When { get() @@ -108,7 +108,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetNonExisting() { - every { projectService.findWithUser("testUser", 1) } returns null + every { projectService.findByUser("testUser", 1) } returns null When { get("/1") @@ -124,7 +124,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testGetExisting() { - every { projectService.findWithUser("testUser", 1) } returns dummyProject + every { projectService.findByUser("testUser", 1) } returns dummyProject When { get("/1") @@ -141,7 +141,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testCreate() { - every { projectService.createForUser("testUser", "test") } returns dummyProject + every { projectService.create("testUser", "test") } returns dummyProject Given { body(Project.Create("test")) @@ -196,7 +196,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testDeleteNonExistent() { - every { projectService.deleteWithUser("testUser", 1) } returns null + every { projectService.delete("testUser", 1) } returns null When { delete("/1") @@ -212,7 +212,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testDelete() { - every { projectService.deleteWithUser("testUser", 1) } returns dummyProject + every { projectService.delete("testUser", 1) } returns dummyProject When { delete("/1") @@ -228,7 +228,7 @@ class ProjectResourceTest { @Test @TestSecurity(user = "testUser", roles = ["openid"]) fun testDeleteNonOwner() { - every { projectService.deleteWithUser("testUser", 1) } throws IllegalArgumentException("User does not own project") + every { projectService.delete("testUser", 1) } throws IllegalArgumentException("User does not own project") When { delete("/1") diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt deleted file mode 100644 index fdf04787..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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 io.mockk.every -import io.mockk.mockk -import io.quarkus.test.junit.QuarkusTest -import org.junit.jupiter.api.Assertions.assertAll -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.opendc.web.server.model.UserAccounting -import org.opendc.web.server.repository.UserAccountingRepository -import java.time.Duration -import java.time.LocalDate -import javax.persistence.EntityExistsException - -/** - * Test suite for the [UserAccountingService]. - */ -@QuarkusTest -class UserAccountingServiceTest { - /** - * The [UserAccountingRepository] that is mocked. - */ - private val repository: UserAccountingRepository = mockk() - - /** - * The [UserAccountingService] instance under test. - */ - private val service: UserAccountingService = UserAccountingService(repository, Duration.ofHours(1)) - - @Test - fun testGetUserDoesNotExist() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - - val accounting = service.getAccounting(userId) - - assertTrue(accounting.periodEnd.isAfter(LocalDate.now())) - assertEquals(0, accounting.simulationTime) - } - - @Test - fun testGetUserDoesExist() { - val userId = "test" - - val now = LocalDate.now() - val periodEnd = now.plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 32 } - - val accounting = service.getAccounting(userId) - - assertAll( - { assertEquals(periodEnd, accounting.periodEnd) }, - { assertEquals(32, accounting.simulationTime) }, - { assertEquals(3600, accounting.simulationTimeBudget) } - ) - } - - @Test - fun testHasBudgetUserDoesNotExist() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudget() { - val userId = "test" - val periodEnd = LocalDate.now().plusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetExceededButPeriodExpired() { - val userId = "test" - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetPeriodExpired() { - val userId = "test" - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetExceeded() { - val userId = "test" - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - - assertFalse(service.hasSimulationBudget(userId)) - } - - @Test - fun testConsumeBudgetNewUser() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - every { repository.save(any()) } returns Unit - - assertFalse(service.consumeSimulationBudget(userId, 10)) - } - - @Test - fun testConsumeBudgetNewUserExceeded() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - every { repository.save(any()) } returns Unit - - assertTrue(service.consumeSimulationBudget(userId, 4000)) - } - - @Test - fun testConsumeBudgetNewUserConflict() { - val userId = "test" - - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns null andThen UserAccounting(userId, periodEnd, 3600) - every { repository.save(any()) } throws EntityExistsException() - every { repository.consumeBudget(any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.simulationTime -= it.invocation.args[1] as Int - true - } - - assertFalse(service.consumeSimulationBudget(userId, 10)) - } - - @Test - fun testConsumeBudgetResetSuccess() { - val userId = "test" - - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - every { repository.resetBudget(any(), any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.periodEnd = it.invocation.args[1] as LocalDate - accounting.simulationTime = it.invocation.args[2] as Int - true - } - - assertTrue(service.consumeSimulationBudget(userId, 4000)) - } - - @Test - fun testInfiniteConflict() { - val userId = "test" - - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - every { repository.consumeBudget(any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.simulationTime -= it.invocation.args[1] as Int - false - } - - assertThrows { service.consumeSimulationBudget(userId, 10) } - } -} -- cgit v1.2.3