diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/java')
29 files changed, 3252 insertions, 0 deletions
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..c5fb208e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -0,0 +1,167 @@ +/* + * 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.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +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 { + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", foreignKey = @ForeignKey(name = "fk_jobs_scenario"), 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; + + /** + * The state of the job. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) + 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<String, ?> 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.updatedAt = 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 A query for jobs that are in the specified state. + */ + public static PanacheQuery<Job> findByState(JobState state) { + return find("state", state); + } + + /** + * 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 <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict. + */ + public boolean updateAtomically(JobState newState, Instant time, int runtime, Map<String, ?> 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)); + Panache.getEntityManager().refresh(this); + return count > 0; + } + + /** + * Determine whether the job is allowed to transition to <code>newState</code>. + * + * @param newState The new state to transition to. + * @return <code>true</code> if the transition to the new state is legal, <code>false</code> otherwise. + */ + public boolean canTransitionTo(JobState newState) { + // Note that we always allow transitions from the state + return newState == this.state + || switch (this.state) { + 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; + }; + } +} 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..3a406683 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.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.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.util.HashSet; +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( + name = "uk_portfolios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_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<Scenario> 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 query of portfolios that belong to the specified project. + */ + public static PanacheQuery<Portfolio> findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * Find the {@link Portfolio} with the specified <code>number</code> belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The portfolio or <code>null</code> 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<Portfolio> 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<Topology> 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<Scenario> 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<ProjectAuthorization> 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..1238f58d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -0,0 +1,174 @@ +/* + * 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.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.io.Serializable; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +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.hibernate.annotations.Type; +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, fetch = FetchType.LAZY) + @MapsId("projectId") + @JoinColumn( + name = "project_id", + updatable = false, + insertable = false, + nullable = false, + foreignKey = @ForeignKey(name = "fk_project_authorizations")) + public Project project; + + /** + * The role of the user in the project. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) + 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 <code>userId</code>. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @return A query returning projects that the user has received authorization for. + */ + public static PanacheQuery<ProjectAuthorization> findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)); + } + + /** + * Find the project with <code>id</code> for the user with the specified <code>userId</code>. + * + * @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 <code>null</code> 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..016e931b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -0,0 +1,199 @@ +/* + * 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.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +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.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( + name = "uk_scenarios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_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, foreignKey = @ForeignKey(name = "fk_scenarios_project")) + public Project project; + + /** + * The {@link Portfolio} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio")) + 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) + @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology")) + 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. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "scenario", + fetch = FetchType.LAZY) + public List<Job> jobs = new ArrayList<>(); + + /** + * 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 query of scenarios that belong to the specified project. + */ + public static PanacheQuery<Scenario> findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * 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 query of scenarios that belong to the specified project and portfolio.. + */ + public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) { + return find( + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)); + } + + /** + * Find the {@link Scenario} with the specified <code>number</code> belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The scenario or <code>null</code> 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..05a1ac12 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -0,0 +1,139 @@ +/* + * 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.hibernate.orm.panache.PanacheQuery; +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( + name = "uk_topologies_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_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<Room> rooms; + + /** + * Construct a {@link Topology} object. + */ + public Topology(Project project, int number, String name, Instant createdAt, List<Room> 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 query of topologies that belong to the specified project. + */ + public static PanacheQuery<Topology> findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * 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..36d27abc --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -0,0 +1,72 @@ +/* + * 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; +import javax.persistence.Table; + +/** + * A workload trace available for simulation. + */ +@Entity +@Table(name = "traces") +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 <code>true</code> when the update succeeded, <code>false</code> 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 <code>true</code> when the update succeeded`, <code>false</code> 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 <code>true</code> when the user still has budget left, <code>false</code> 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/rest/BaseProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java new file mode 100644 index 00000000..44d2d569 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java @@ -0,0 +1,50 @@ +/* + * 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.rest; + +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; + +/** + * DTO-conversions for the base protocol. + */ +public final class BaseProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private BaseProtocol() {} + + /** + * 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(toDto(workload.trace), workload.samplingFraction); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toDto(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/rest/SchedulerResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java new file mode 100644 index 00000000..0fd58182 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java @@ -0,0 +1,52 @@ +/* + * 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.rest; + +import java.util.List; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +/** + * A resource representing the available schedulers that can be used during experiments. + */ +@Produces("application/json") +@Path("/schedulers") +public final class SchedulerResource { + /** + * Obtain all available schedulers. + */ + @GET + public List<String> getAll() { + return List.of( + "mem", + "mem-inv", + "core-mem", + "core-mem-inv", + "active-servers", + "active-servers-inv", + "provisioned-cores", + "provisioned-cores-inv", + "random"); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java new file mode 100644 index 00000000..7316c93f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java @@ -0,0 +1,70 @@ +/* + * 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.rest; + +import java.util.List; +import java.util.stream.Stream; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Trace; + +/** + * A resource representing the workload traces available in the OpenDC instance. + */ +@Produces("application/json") +@Path("/traces") +public final class TraceResource { + /** + * Obtain all available traces. + */ + @GET + public List<org.opendc.web.proto.Trace> getAll() { + Stream<Trace> entities = Trace.streamAll(); + return entities.map(TraceResource::toDto).toList(); + } + + /** + * Obtain trace information by identifier. + */ + @GET + @Path("{id}") + public org.opendc.web.proto.Trace get(@PathParam("id") String id) { + Trace trace = Trace.findById(id); + + if (trace == null) { + throw new WebApplicationException("Trace not found", 404); + } + + return toDto(trace); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toDto(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/rest/error/MissingKotlinParameterExceptionMapper.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java new file mode 100644 index 00000000..3b6be42e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * 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.rest.error; + +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.opendc.web.proto.ProtocolError; + +/** + * An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson. + */ +@Provider +public final class MissingKotlinParameterExceptionMapper implements ExceptionMapper<MissingKotlinParameterException> { + @Override + public Response toResponse(MissingKotlinParameterException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ProtocolError( + Response.Status.BAD_REQUEST.getStatusCode(), + "Field " + exception.getParameter().getName() + " is missing from body.")) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java new file mode 100644 index 00000000..ad1bb05e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java @@ -0,0 +1,51 @@ +/* + * 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.rest.error; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.opendc.web.proto.ProtocolError; + +/** + * Helper class to transform a {@link WebApplicationException} into an JSON error response. + */ +@Provider +public final class WebApplicationExceptionMapper implements ExceptionMapper<WebApplicationException> { + @Override + public Response toResponse(WebApplicationException exception) { + int code = exception.getResponse().getStatus(); + + String message = exception.getMessage(); + if (message == null) { + message = "Unknown error"; + } + + return Response.status(code) + .entity(new ProtocolError(code, message)) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java new file mode 100644 index 00000000..dff52526 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java @@ -0,0 +1,110 @@ +/* + * 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.rest.runner; + +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.service.JobService; + +/** + * A resource representing the available simulation jobs. + */ +@Produces("application/json") +@Path("/jobs") +@RolesAllowed("runner") +public final class JobResource { + /** + * The {@link JobService} for helping manage the job lifecycle. + */ + private final JobService jobService; + + /** + * Construct a {@link JobResource} instance. + * + * @param jobService The {@link JobService} for managing the job lifecycle. + */ + public JobResource(JobService jobService) { + this.jobService = jobService; + } + + /** + * Obtain all pending simulation jobs. + */ + @GET + public List<org.opendc.web.proto.runner.Job> queryPending() { + return Job.findByState(JobState.PENDING).list().stream() + .map(RunnerProtocol::toDto) + .toList(); + } + + /** + * Get a job by identifier. + */ + @GET + @Path("{job}") + public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) { + Job job = Job.findById(id); + + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } + + return RunnerProtocol.toDto(job); + } + + /** + * Atomically update the state of a job. + */ + @POST + @Path("{job}") + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.runner.Job update( + @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) { + Job job = Job.findById(id); + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } + + try { + jobService.updateJob(job, update.getState(), update.getRuntime(), update.getResults()); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(e, 400); + } catch (IllegalStateException e) { + throw new WebApplicationException(e, 409); + } + + return RunnerProtocol.toDto(job); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java new file mode 100644 index 00000000..6bf65d97 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java @@ -0,0 +1,78 @@ +/* + * 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.rest.runner; + +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; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the runner protocol. + */ +public final class RunnerProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private RunnerProtocol() {} + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, toDto(job.scenario), job.state, job.createdAt, job.updatedAt, job.runtime, job.results); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + toDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toDto(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/rest/user/PortfolioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java new file mode 100644 index 00000000..d1fc980d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java @@ -0,0 +1,161 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * A resource representing the portfolios of a project. + */ +@Produces("application/json") +@Path("/projects/{project}/portfolios") +@RolesAllowed("openid") +public final class PortfolioResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link PortfolioResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public PortfolioResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Get all portfolios that belong to the specified project. + */ + @GET + public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).list().stream() + .map((p) -> UserProtocol.toDto(p, auth)) + .toList(); + } + + /** + * Create a portfolio for this project. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Portfolio create( + @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Portfolio.Create request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + 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 UserProtocol.toDto(portfolio, auth); + } + + /** + * Obtain a portfolio by its identifier. + */ + @GET + @Path("{portfolio}") + public org.opendc.web.proto.user.Portfolio get( + @PathParam("project") long projectId, @PathParam("portfolio") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + return UserProtocol.toDto(portfolio, auth); + } + + /** + * Delete a portfolio. + */ + @DELETE + @Path("{portfolio}") + @Transactional + public org.opendc.web.proto.user.Portfolio delete( + @PathParam("project") long projectId, @PathParam("portfolio") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java new file mode 100644 index 00000000..a058cd31 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java @@ -0,0 +1,159 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +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; +import org.opendc.web.server.service.UserAccountingService; + +/** + * A resource representing the scenarios of a portfolio. + */ +@Path("/projects/{project}/portfolios/{portfolio}/scenarios") +@RolesAllowed("openid") +@Produces("application/json") +public final class PortfolioScenarioResource { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link PortfolioScenarioResource}. + * + * @param accountingService The {@link UserAccountingService} instance to use. + * @param identity The {@link SecurityIdentity} of the current user. + */ + public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; + this.identity = identity; + } + + /** + * Get all scenarios that belong to the specified portfolio. + */ + @GET + public List<org.opendc.web.proto.user.Scenario> get( + @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return org.opendc.web.server.model.Scenario.findByPortfolio(projectId, portfolioNumber).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + + /** + * Create a scenario for this portfolio. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Scenario create( + @PathParam("project") long projectId, + @PathParam("portfolio") int portfolioNumber, + @Valid org.opendc.web.proto.user.Scenario.Create request) { + // User must have access to project + String userId = identity.getPrincipal().getName(); + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new WebApplicationException("Referred topology does not exist", 400); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new WebApplicationException("Referred trace does not exist", 400); + } + + 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()); + scenario.persist(); + + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + job.persist(); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.jobs.add(job); + portfolio.scenarios.add(scenario); + + return UserProtocol.toDto(scenario, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java new file mode 100644 index 00000000..da47c3ff --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java @@ -0,0 +1,131 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * A resource representing the created projects. + */ +@Produces("application/json") +@Path("/projects") +@RolesAllowed("openid") +public final class ProjectResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link ProjectResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public ProjectResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Obtain all the projects of the current user. + */ + @GET + public List<org.opendc.web.proto.user.Project> getAll() { + return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream() + .map(UserProtocol::toDto) + .toList(); + } + + /** + * Create a new project for the current user. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) { + Instant now = Instant.now(); + Project entity = new Project(request.getName(), now); + entity.persist(); + + ProjectAuthorization authorization = + new ProjectAuthorization(entity, identity.getPrincipal().getName(), ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return UserProtocol.toDto(authorization); + } + + /** + * Obtain a single project by its identifier. + */ + @GET + @Path("{project}") + public org.opendc.web.proto.user.Project get(@PathParam("project") long id) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return UserProtocol.toDto(auth); + } + + /** + * Delete a project. + */ + @DELETE + @Path("{project}") + @Transactional + public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canDelete()) { + throw new WebApplicationException("Not allowed to delete project", 403); + } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = UserProtocol.toDto(auth); + auth.project.delete(); + return project; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java new file mode 100644 index 00000000..cf933c32 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java @@ -0,0 +1,127 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; + +/** + * A resource representing the scenarios of a portfolio. + */ +@Produces("application/json") +@Path("/projects/{project}/scenarios") +@RolesAllowed("openid") +public final class ScenarioResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link ScenarioResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public ScenarioResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Obtain the scenarios belonging to a project. + */ + @GET + public List<org.opendc.web.proto.user.Scenario> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return Scenario.findByProject(projectId).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + + /** + * Obtain a scenario by its identifier. + */ + @GET + @Path("{scenario}") + public org.opendc.web.proto.user.Scenario get( + @PathParam("project") long projectId, @PathParam("scenario") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + Scenario scenario = Scenario.findByProject(projectId, number); + + if (scenario == null) { + throw new WebApplicationException("Scenario not found", 404); + } + + return UserProtocol.toDto(scenario, auth); + } + + /** + * Delete a scenario. + */ + @DELETE + @Path("{scenario}") + @Transactional + public org.opendc.web.proto.user.Scenario delete( + @PathParam("project") long projectId, @PathParam("scenario") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { + throw new WebApplicationException("Scenario not found", 404); + } + + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java new file mode 100644 index 00000000..2b66b64b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java @@ -0,0 +1,198 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; + +/** + * A resource representing the constructed datacenter topologies. + */ +@Produces("application/json") +@Path("/projects/{project}/topologies") +@RolesAllowed("openid") +public final class TopologyResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link TopologyResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public TopologyResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Get all topologies that belong to the specified project. + */ + @GET + public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).list().stream() + .map((t) -> UserProtocol.toDto(t, auth)) + .toList(); + } + + /** + * Create a topology for this project. + */ + @POST + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.user.Topology create( + @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + 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 UserProtocol.toDto(topology, auth); + } + + /** + * Obtain a topology by its number. + */ + @GET + @Path("{topology}") + public org.opendc.web.proto.user.Topology get( + @PathParam("project") long projectId, @PathParam("topology") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } + + Topology topology = Topology.findByProject(projectId, number); + + if (topology == null) { + throw new WebApplicationException("Topology not found", 404); + } + + return UserProtocol.toDto(topology, auth); + } + + /** + * Update the specified topology by its number. + */ + @PUT + @Path("{topology}") + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.user.Topology update( + @PathParam("project") long projectId, + @PathParam("topology") int number, + @Valid org.opendc.web.proto.user.Topology.Update request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + throw new WebApplicationException("Topology not found", 404); + } + + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return UserProtocol.toDto(entity, auth); + } + + /** + * Delete the specified topology. + */ + @Path("{topology}") + @DELETE + @Transactional + public org.opendc.web.proto.user.Topology delete( + @PathParam("project") long projectId, @PathParam("topology") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + throw new WebApplicationException("Topology not found", 404); + } + + entity.updatedAt = Instant.now(); + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java new file mode 100644 index 00000000..8196a9d6 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java @@ -0,0 +1,132 @@ +/* + * 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.rest.user; + +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Project; +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.rest.BaseProtocol; + +/** + * DTO-conversions for the user protocol. + */ +public final class UserProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private UserProtocol() {} + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + toDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(UserProtocol::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 Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + toDto(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 Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + toDto(auth), + toSummaryDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * 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, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java new file mode 100644 index 00000000..c3fb2866 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java @@ -0,0 +1,73 @@ +/* + * 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.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import org.opendc.web.proto.user.User; +import org.opendc.web.proto.user.UserAccounting; +import org.opendc.web.server.service.UserAccountingService; + +/** + * A resource representing the active user. + */ +@Produces("application/json") +@Path("/users") +@RolesAllowed("openid") +public final class UserResource { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link UserResource}. + * + * @param accountingService The {@link UserAccountingService} instance to use. + * @param identity The {@link SecurityIdentity} of the current user. + */ + public UserResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; + this.identity = identity; + } + + /** + * Get the current active user data. + */ + @GET + @Path("me") + public User get() { + 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/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..ed0eaf9c --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -0,0 +1,81 @@ +/* + * 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.Map; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * A service for managing the lifecycle of a job and ensuring that the user does not consume + * too much simulation resources. + */ +@ApplicationScoped +public final class JobService { + /** + * The {@link UserAccountingService} responsible for accounting the simulation time of users. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link JobService} instance. + * + * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users. + */ + public JobService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Update the job state. + * + * @param job The {@link Job} to update. + * @param newState The new state to transition the job to. + * @param runtime The runtime (in seconds) consumed by the simulation jbo so far. + * @param results The results to attach to the job. + * @throws IllegalArgumentException if the state transition is invalid. + * @throws IllegalStateException if someone tries to update the job concurrently. + */ + public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) { + JobState state = job.state; + + if (!job.canTransitionTo(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 - job.runtime); + + // Check whether the user still has any simulation budget left + if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { + nextState = JobState.FAILED; // User has consumed all their budget; cancel the job + } + + if (!job.updateAtomically(nextState, now, runtime, results)) { + throw new IllegalStateException("Conflicting update"); + } + } +} 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 <code>userId</code>. 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 <code>userId</code> has any remaining simulation budget. + * + * @param userId The unique identifier of the user. + * @return <code>true</code> when the user still has budget left, <code>false</code> otherwise. + */ + public boolean hasSimulationBudget(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting == null) { + return true; + } + return accounting.hasSimulationBudget(); + } + + /** + * Consume <code>seconds</code> from the simulation budget of the user with <code>userId</code>. + * + * @param userId The unique identifier of the user. + * @param seconds The seconds to consume from the simulation budget. + * @return <code>true</code> if the user has consumed his full budget or <code>false</code> 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/util/DevSecurityOverrideFilter.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java new file mode 100644 index 00000000..de4478cb --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java @@ -0,0 +1,64 @@ +/* + * 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.util; + +import io.quarkus.arc.properties.IfBuildProperty; +import java.security.Principal; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; + +/** + * Helper class to disable security for the OpenDC web API when in development mode. + */ +@Provider +@PreMatching +@IfBuildProperty(name = "opendc.security.enabled", stringValue = "false") +public class DevSecurityOverrideFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) { + requestContext.setSecurityContext(new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return () -> "anon"; + } + + @Override + public boolean isUserInRole(String role) { + return true; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return "basic"; + } + }); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java new file mode 100644 index 00000000..c30edcbf --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java @@ -0,0 +1,39 @@ +/* + * 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.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.kotlin.KotlinModule; +import io.quarkus.jackson.ObjectMapperCustomizer; +import javax.inject.Singleton; + +/** + * Helper class to register the Kotlin Jackson module. + */ +@Singleton +public final class KotlinModuleCustomizer implements ObjectMapperCustomizer { + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.registerModule(new KotlinModule.Builder().build()); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java new file mode 100644 index 00000000..e46c74ed --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java @@ -0,0 +1,39 @@ +/* + * 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.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.hypersistence.utils.hibernate.type.util.ObjectMapperSupplier; +import io.quarkus.runtime.annotations.RegisterForReflection; +import javax.enterprise.inject.spi.CDI; + +/** + * A supplier for an {@link ObjectMapper} used by the Hypersistence utilities. + */ +@RegisterForReflection +public class QuarkusObjectMapperSupplier implements ObjectMapperSupplier { + @Override + public ObjectMapper get() { + return CDI.current().select(ObjectMapper.class).get(); + } +} 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 new file mode 100644 index 00000000..0331eacf --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.util.runner; + +import java.util.Map; +import javax.enterprise.context.ApplicationScoped; +import javax.transaction.Transactional; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.opendc.web.proto.JobState; +import org.opendc.web.runner.JobManager; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.rest.runner.RunnerProtocol; +import org.opendc.web.server.service.JobService; + +/** + * Implementation of {@link JobManager} that interfaces directly with the database without overhead of the REST API. + */ +@ApplicationScoped +public class QuarkusJobManager implements JobManager { + /** + * The {@link JobService} used to manage the job's lifecycle. + */ + private final JobService jobService; + + /** + * Construct a {@link QuarkusJobManager}. + * + * @param jobService The {@link JobService} for managing the job's lifecycle. + */ + public QuarkusJobManager(JobService jobService) { + this.jobService = jobService; + } + + @Transactional + @Nullable + @Override + public org.opendc.web.proto.runner.Job findNext() { + var job = Job.findByState(JobState.PENDING).firstResult(); + if (job == null) { + return null; + } + + return RunnerProtocol.toDto(job); + } + + @Transactional + @Override + public boolean claim(long id) { + return updateState(id, JobState.CLAIMED, 0, null); + } + + @Transactional + @Override + public boolean heartbeat(long id, int runtime) { + return updateState(id, JobState.RUNNING, runtime, null); + } + + @Transactional + @Override + public void fail(long id, int runtime) { + updateState(id, JobState.FAILED, runtime, null); + } + + @Transactional + @Override + public void finish(long id, int runtime, @NotNull Map<String, ?> results) { + updateState(id, JobState.FINISHED, runtime, results); + } + + /** + * Helper method to update the state of a job. + * + * @param id The unique id of the job. + * @param newState The new state to transition to. + * @param runtime The runtime of the job. + * @param results The results of the job. + * @return <code>true</code> if the operation succeeded, <code>false</code> otherwise. + */ + private boolean updateState(long id, JobState newState, int runtime, Map<String, ?> results) { + Job job = Job.findById(id); + + if (job == null) { + return false; + } + + try { + jobService.updateJob(job, newState, runtime, results); + return true; + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } + } +} |
