diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src/main')
26 files changed, 807 insertions, 941 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 index 14fd3e2a..c5fb208e 100644 --- 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 @@ -22,18 +22,22 @@ 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.List; 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.OneToOne; import javax.persistence.Table; import org.hibernate.annotations.Type; import org.opendc.web.proto.JobState; @@ -54,8 +58,8 @@ import org.opendc.web.proto.JobState; """) }) public class Job extends PanacheEntity { - @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) - @JoinColumn(name = "scenario_id", nullable = false) + @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) @@ -74,12 +78,14 @@ public class Job extends PanacheEntity { * The instant at which the job was updated. */ @Column(name = "updated_at", nullable = false) - public Instant updatedAt = createdAt; + public Instant updatedAt; /** * The state of the job. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public JobState state = JobState.PENDING; /** @@ -102,6 +108,7 @@ public class Job extends PanacheEntity { this.createdBy = createdBy; this.scenario = scenario; this.createdAt = createdAt; + this.updatedAt = createdAt; this.repeats = repeats; } @@ -114,10 +121,10 @@ public class Job extends PanacheEntity { * Find {@link Job}s in the specified {@link JobState}. * * @param state The state of the jobs to find. - * @return The list of jobs that are in the specified state. + * @return A query for jobs that are in the specified state. */ - public static List<Job> findByState(JobState state) { - return find("state", state).list(); + public static PanacheQuery<Job> findByState(JobState state) { + return find("state", state); } /** @@ -137,6 +144,24 @@ public class Job extends PanacheEntity { .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 index 4c3af570..3a406683 100644 --- 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 @@ -23,9 +23,9 @@ 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.List; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -48,8 +48,12 @@ import org.opendc.web.proto.Targets; @Entity @Table( name = "portfolios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")}) + 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( @@ -112,11 +116,10 @@ public class Portfolio extends PanacheEntity { * Find all {@link Portfolio}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of portfolios that belong to the specified project. + * @return The query of portfolios that belong to the specified project. */ - public static List<Portfolio> findByProject(long projectId) { - return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Portfolio> findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)); } /** 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 index c10fcc64..1238f58d 100644 --- 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 @@ -23,20 +23,25 @@ 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.List; 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; /** @@ -64,15 +69,22 @@ public class ProjectAuthorization extends PanacheEntityBase { /** * The project that the user is authorized to participate in. */ - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @MapsId("projectId") - @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) + @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. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public ProjectRole role; /** @@ -93,11 +105,10 @@ public class ProjectAuthorization extends PanacheEntityBase { * 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 list of projects that the user has received authorization for. + * @return A query returning projects that the user has received authorization for. */ - public static List<ProjectAuthorization> findByUser(String userId) { - return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)) - .list(); + public static PanacheQuery<ProjectAuthorization> findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", 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 index 9381f9be..016e931b 100644 --- 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 @@ -23,18 +23,22 @@ 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.OneToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.hibernate.annotations.Type; @@ -46,8 +50,12 @@ import org.opendc.web.proto.OperationalPhenomena; @Entity @Table( name = "scenarios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")}) + 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( @@ -68,14 +76,14 @@ public class Scenario extends PanacheEntity { * The {@link Project} to which this scenario belongs. */ @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = 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) + @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio")) public Portfolio portfolio; /** @@ -100,6 +108,7 @@ public class Scenario extends PanacheEntity { * Topology details of the scenario. */ @ManyToOne(optional = false) + @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology")) public Topology topology; /** @@ -118,8 +127,11 @@ public class Scenario extends PanacheEntity { /** * The {@link Job} associated with the scenario. */ - @OneToOne(cascade = {CascadeType.ALL}) - public Job job; + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "scenario", + fetch = FetchType.LAZY) + public List<Job> jobs = new ArrayList<>(); /** * Construct a {@link Scenario} object. @@ -152,11 +164,10 @@ public class Scenario extends PanacheEntity { * Find all {@link Scenario}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of scenarios that belong to the specified project. + * @return The query of scenarios that belong to the specified project. */ - public static List<Scenario> findByProject(long projectId) { - return find("#Scenario.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Scenario> findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)); } /** @@ -164,13 +175,12 @@ public class Scenario extends PanacheEntity { * * @param projectId The unique identifier of the project. * @param number The number of the portfolio. - * @return The list of scenarios that belong to the specified project and portfolio.. + * @return The query of scenarios that belong to the specified project and portfolio.. */ - public static List<Scenario> findByPortfolio(long projectId, int number) { + public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) { return find( - "#Scenario.findByPortfolio", - Parameters.with("projectId", projectId).and("number", number)) - .list(); + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)); } /** 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 index 6ec83f78..05a1ac12 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -44,8 +45,12 @@ import org.opendc.web.proto.Room; @Entity @Table( name = "topologies", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")}) + 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( @@ -112,11 +117,10 @@ public class Topology extends PanacheEntity { * Find all [Topology]s that belong to [project][projectId]. * * @param projectId The unique identifier of the project. - * @return The list of topologies that belong to the specified project. + * @return The query of topologies that belong to the specified project. */ - public static List<Topology> findByProject(long projectId) { - return find("#Topology.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery<Topology> findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)); } /** 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 index f73c8494..36d27abc 100644 --- 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 @@ -26,11 +26,13 @@ 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. 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/TraceResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java index 2b1efb02..7316c93f 100644 --- 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 @@ -43,7 +43,7 @@ public final class TraceResource { @GET public List<org.opendc.web.proto.Trace> getAll() { Stream<Trace> entities = Trace.streamAll(); - return entities.map(TraceResource::toUserDto).toList(); + return entities.map(TraceResource::toDto).toList(); } /** @@ -58,13 +58,13 @@ public final class TraceResource { throw new WebApplicationException("Trace not found", 404); } - return toUserDto(trace); + return toDto(trace); } /** * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. */ - public static org.opendc.web.proto.Trace toUserDto(Trace trace) { + 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/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java index 134c6814..dff52526 100644 --- 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 @@ -33,6 +33,8 @@ 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; /** @@ -43,14 +45,14 @@ import org.opendc.web.server.service.JobService; @RolesAllowed("runner") public final class JobResource { /** - * The {@link JobService} responsible for managing the jobs. + * The {@link JobService} for helping manage the job lifecycle. */ private final JobService jobService; /** * Construct a {@link JobResource} instance. * - * @param jobService The {@link JobService} responsible for managing the jobs. + * @param jobService The {@link JobService} for managing the job lifecycle. */ public JobResource(JobService jobService) { this.jobService = jobService; @@ -61,7 +63,9 @@ public final class JobResource { */ @GET public List<org.opendc.web.proto.runner.Job> queryPending() { - return jobService.listPending(); + return Job.findByState(JobState.PENDING).list().stream() + .map(RunnerProtocol::toDto) + .toList(); } /** @@ -70,12 +74,13 @@ public final class JobResource { @GET @Path("{job}") public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) { - org.opendc.web.proto.runner.Job job = jobService.findById(id); + Job job = Job.findById(id); + if (job == null) { throw new WebApplicationException("Job not found", 404); } - return job; + return RunnerProtocol.toDto(job); } /** @@ -87,17 +92,19 @@ public final class JobResource { @Transactional public org.opendc.web.proto.runner.Job update( @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) { - try { - var job = jobService.updateState(id, update.getState(), update.getRuntime(), update.getResults()); - if (job == null) { - throw new WebApplicationException("Job not found", 404); - } + Job job = Job.findById(id); + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } - return job; + 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 index e8e05f97..d1fc980d 100644 --- 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 @@ -23,10 +23,12 @@ 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; @@ -34,7 +36,8 @@ 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.service.PortfolioService; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; /** * A resource representing the portfolios of a project. @@ -44,11 +47,6 @@ import org.opendc.web.server.service.PortfolioService; @RolesAllowed("openid") public final class PortfolioResource { /** - * The service for managing the user portfolios. - */ - private final PortfolioService portfolioService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -56,11 +54,9 @@ public final class PortfolioResource { /** * Construct a {@link PortfolioResource}. * - * @param portfolioService The {@link PortfolioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioResource(PortfolioService portfolioService, SecurityIdentity identity) { - this.portfolioService = portfolioService; + public PortfolioResource(SecurityIdentity identity) { this.identity = identity; } @@ -69,7 +65,17 @@ public final class PortfolioResource { */ @GET public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long projectId) { - return portfolioService.findByUser(identity.getPrincipal().getName(), 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(); } /** @@ -77,14 +83,29 @@ public final class PortfolioResource { */ @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) { - var portfolio = portfolioService.create(identity.getPrincipal().getName(), projectId, request); - if (portfolio == null) { + // 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); } - return portfolio; + 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); } /** @@ -94,12 +115,21 @@ public final class PortfolioResource { @Path("{portfolio}") public org.opendc.web.proto.user.Portfolio get( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.findByUser(identity.getPrincipal().getName(), projectId, 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 portfolio; + return UserProtocol.toDto(portfolio, auth); } /** @@ -110,11 +140,22 @@ public final class PortfolioResource { @Transactional public org.opendc.web.proto.user.Portfolio delete( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.delete(identity.getPrincipal().getName(), projectId, number); - if (portfolio == null) { + // 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); } - return portfolio; + 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 index a6db7c54..a058cd31 100644 --- 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 @@ -23,28 +23,39 @@ 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.user.Scenario; -import org.opendc.web.server.service.ScenarioService; +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 scenarios. + * The service for managing the user accounting. */ - private final ScenarioService scenarioService; + private final UserAccountingService accountingService; /** * The identity of the current user. @@ -54,11 +65,11 @@ public final class PortfolioScenarioResource { /** * Construct a {@link PortfolioScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. + * @param accountingService The {@link UserAccountingService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; this.identity = identity; } @@ -66,8 +77,19 @@ public final class PortfolioScenarioResource { * Get all scenarios that belong to the specified portfolio. */ @GET - public List<Scenario> get(@PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { - return scenarioService.findAll(identity.getPrincipal().getName(), projectId, portfolioNumber); + 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(); } /** @@ -75,15 +97,63 @@ public final class PortfolioScenarioResource { */ @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) { - var scenario = scenarioService.create(identity.getPrincipal().getName(), projectId, portfolioNumber, request); - if (scenario == null) { + // 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); } - return scenario; + 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 index b0b8eb4e..da47c3ff 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -35,7 +36,9 @@ 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.service.ProjectService; +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. @@ -45,11 +48,6 @@ import org.opendc.web.server.service.ProjectService; @RolesAllowed("openid") public final class ProjectResource { /** - * The service for managing the user projects. - */ - private final ProjectService projectService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -57,11 +55,9 @@ public final class ProjectResource { /** * Construct a {@link ProjectResource}. * - * @param projectService The {@link ProjectService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ProjectResource(ProjectService projectService, SecurityIdentity identity) { - this.projectService = projectService; + public ProjectResource(SecurityIdentity identity) { this.identity = identity; } @@ -70,7 +66,9 @@ public final class ProjectResource { */ @GET public List<org.opendc.web.proto.user.Project> getAll() { - return projectService.findByUser(identity.getPrincipal().getName()); + return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream() + .map(UserProtocol::toDto) + .toList(); } /** @@ -80,7 +78,17 @@ public final class ProjectResource { @Transactional @Consumes("application/json") public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) { - return projectService.create(identity.getPrincipal().getName(), request.getName()); + 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); } /** @@ -89,12 +97,14 @@ public final class ProjectResource { @GET @Path("{project}") public org.opendc.web.proto.user.Project get(@PathParam("project") long id) { - var project = projectService.findByUser(identity.getPrincipal().getName(), id); - if (project == null) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { throw new WebApplicationException("Project not found", 404); } - return project; + return UserProtocol.toDto(auth); } /** @@ -104,15 +114,18 @@ public final class ProjectResource { @Path("{project}") @Transactional public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) { - try { - var project = projectService.delete(identity.getPrincipal().getName(), id); - if (project == null) { - throw new WebApplicationException("Project not found", 404); - } + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); - return project; - } catch (IllegalArgumentException e) { - throw new WebApplicationException(e.getMessage(), 403); + 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 index a6838148..cf933c32 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -31,7 +32,8 @@ 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.service.ScenarioService; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; /** * A resource representing the scenarios of a portfolio. @@ -41,11 +43,6 @@ import org.opendc.web.server.service.ScenarioService; @RolesAllowed("openid") public final class ScenarioResource { /** - * The service for managing the user scenarios. - */ - private final ScenarioService scenarioService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -53,27 +50,52 @@ public final class ScenarioResource { /** * Construct a {@link ScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + 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) { - var scenario = scenarioService.findOne(identity.getPrincipal().getName(), projectId, 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 scenario; + return UserProtocol.toDto(scenario, auth); } /** @@ -84,11 +106,22 @@ public final class ScenarioResource { @Transactional public org.opendc.web.proto.user.Scenario delete( @PathParam("project") long projectId, @PathParam("scenario") int number) { - var scenario = scenarioService.delete(identity.getPrincipal().getName(), projectId, number); - if (scenario == null) { + // 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); } - return scenario; + 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 index 54afc1ce..2b66b64b 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -36,7 +37,9 @@ 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.service.TopologyService; +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. @@ -46,11 +49,6 @@ import org.opendc.web.server.service.TopologyService; @RolesAllowed("openid") public final class TopologyResource { /** - * The service for managing the user topologies. - */ - private final TopologyService topologyService; - - /** * The identity of the current user. */ private final SecurityIdentity identity; @@ -58,11 +56,9 @@ public final class TopologyResource { /** * Construct a {@link TopologyResource}. * - * @param topologyService The {@link TopologyService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public TopologyResource(TopologyService topologyService, SecurityIdentity identity) { - this.topologyService = topologyService; + public TopologyResource(SecurityIdentity identity) { this.identity = identity; } @@ -71,7 +67,17 @@ public final class TopologyResource { */ @GET public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long projectId) { - return topologyService.findAll(identity.getPrincipal().getName(), 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(); } /** @@ -82,13 +88,26 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology create( @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) { - var topology = topologyService.create(identity.getPrincipal().getName(), projectId, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); } - return topology; + 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); } /** @@ -98,13 +117,21 @@ public final class TopologyResource { @Path("{topology}") public org.opendc.web.proto.user.Topology get( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.findOne(identity.getPrincipal().getName(), projectId, 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 topology; + return UserProtocol.toDto(topology, auth); } /** @@ -118,13 +145,26 @@ public final class TopologyResource { @PathParam("project") long projectId, @PathParam("topology") int number, @Valid org.opendc.web.proto.user.Topology.Update request) { - var topology = topologyService.update(identity.getPrincipal().getName(), projectId, number, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + 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); } - return topology; + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return UserProtocol.toDto(entity, auth); } /** @@ -135,12 +175,24 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology delete( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.delete(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + 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); } - return topology; + 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/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java index 47f44d27..ed0eaf9c 100644 --- 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 @@ -23,122 +23,59 @@ package org.opendc.web.server.service; import java.time.Instant; -import java.util.List; import java.util.Map; import javax.enterprise.context.ApplicationScoped; import org.opendc.web.proto.JobState; import org.opendc.web.server.model.Job; /** - * Service for managing {@link Job}s. + * 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 service for managing the user accounting. + * 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} instance to use. + * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users. */ public JobService(UserAccountingService accountingService) { this.accountingService = accountingService; } /** - * Query the pending simulation jobs. - */ - public List<org.opendc.web.proto.runner.Job> listPending() { - return Job.findByState(JobState.PENDING).stream() - .map(JobService::toRunnerDto) - .toList(); - } - - /** - * Find a job by its identifier. - */ - public org.opendc.web.proto.runner.Job findById(long id) { - Job job = Job.findById(id); - - if (job == null) { - return null; - } - - return toRunnerDto(job); - } - - /** - * Atomically update the state of a {@link Job}. + * Update the job state. * - * @param id The identifier of the job. - * @param newState The next state for the job. - * @param runtime The runtime of the job (in seconds). - * @param results The potential results of the job. + * @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 org.opendc.web.proto.runner.Job updateState( - long id, JobState newState, int runtime, Map<String, ?> results) { - Job entity = Job.findById(id); - if (entity == null) { - return null; - } + public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) { + JobState state = job.state; - JobState state = entity.state; - if (!isTransitionLegal(state, newState)) { + 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 - entity.runtime); + int consumedBudget = Math.min(1, runtime - job.runtime); // Check whether the user still has any simulation budget left - if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) - && nextState == JobState.RUNNING) { + if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { nextState = JobState.FAILED; // User has consumed all their budget; cancel the job } - if (!entity.updateAtomically(nextState, now, runtime, results)) { + if (!job.updateAtomically(nextState, now, runtime, results)) { throw new IllegalStateException("Conflicting update"); } - - return toRunnerDto(entity); - } - - /** - * Determine whether the transition from [this] to [newState] is legal. - */ - public static boolean isTransitionLegal(JobState currentState, JobState newState) { - // Note that we always allow transitions from the state - return newState == currentState - || switch (currentState) { - case PENDING -> newState == JobState.CLAIMED; - case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; - case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; - case FINISHED, FAILED -> false; - }; - } - - /** - * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. - */ - public static org.opendc.web.proto.user.Job toUserDto(Job job) { - return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); - } - - /** - * Convert a {@link Job} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) { - return new org.opendc.web.proto.runner.Job( - job.id, - ScenarioService.toRunnerDto(job.scenario), - job.state, - job.createdAt, - job.updatedAt, - job.runtime, - job.results); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java deleted file mode 100644 index 94da5195..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Portfolio}s. - */ -@ApplicationScoped -public final class PortfolioService { - /** - * List all {@link Portfolio}s that belong a certain project. - */ - public List<org.opendc.web.proto.user.Portfolio> findByUser(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Portfolio.findByProject(projectId).stream() - .map((p) -> toUserDto(p, auth)) - .toList(); - } - - /** - * Find a {@link Portfolio} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Portfolio portfolio = Portfolio.findByProject(projectId, number); - - if (portfolio == null) { - return null; - } - - return toUserDto(portfolio, auth); - } - - /** - * Delete the portfolio with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio entity = Portfolio.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Portfolio} with the specified name. - */ - public org.opendc.web.proto.user.Portfolio create( - String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocatePortfolio(now); - - Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); - - project.portfolios.add(portfolio); - portfolio.persist(); - - return toUserDto(portfolio, auth); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. - */ - public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Portfolio( - portfolio.id, - portfolio.number, - ProjectService.toUserDto(auth), - portfolio.name, - portfolio.targets, - portfolio.scenarios.stream().map(ScenarioService::toSummaryDto).toList()); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. - */ - public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { - return new org.opendc.web.proto.user.Portfolio.Summary( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } - - /** - * Convert a {@link Portfolio} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) { - return new org.opendc.web.proto.runner.Portfolio( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java deleted file mode 100644 index aeef664e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.user.ProjectRole; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Project}s. - */ -@ApplicationScoped -public final class ProjectService { - /** - * List all projects for the user with the specified <code>userId</code>. - */ - public List<org.opendc.web.proto.user.Project> findByUser(String userId) { - return ProjectAuthorization.findByUser(userId).stream() - .map(ProjectService::toUserDto) - .toList(); - } - - /** - * Obtain the project with the specified <code>id</code> for the user with the specified <code>userId</code>. - */ - public org.opendc.web.proto.user.Project findByUser(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - return toUserDto(auth); - } - - /** - * Create a new {@link Project} for the user with the specified <code>userId</code>. - */ - public org.opendc.web.proto.user.Project create(String userId, String name) { - Instant now = Instant.now(); - Project entity = new Project(name, now); - entity.persist(); - - ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER); - - entity.authorizations.add(authorization); - authorization.persist(); - - return toUserDto(authorization); - } - - /** - * Delete a project by its identifier. - * - * @param userId The user that invokes the action. - * @param id The identifier of the project. - */ - public org.opendc.web.proto.user.Project delete(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - if (!auth.canDelete()) { - throw new IllegalArgumentException("Not allowed to delete project"); - } - - auth.project.updatedAt = Instant.now(); - org.opendc.web.proto.user.Project project = toUserDto(auth); - auth.project.delete(); - return project; - } - - /** - * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. - */ - public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) { - Project project = auth.project; - return new org.opendc.web.proto.user.Project( - project.id, project.name, project.createdAt, project.updatedAt, auth.role); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java deleted file mode 100644 index 6a70db1e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.JobState; -import org.opendc.web.server.model.Job; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Scenario; -import org.opendc.web.server.model.Topology; -import org.opendc.web.server.model.Trace; -import org.opendc.web.server.model.Workload; - -/** - * Service for managing {@link Scenario}s. - */ -@ApplicationScoped -public final class ScenarioService { - /** - * The service for managing the user accounting. - */ - private final UserAccountingService accountingService; - - /** - * Construct a {@link ScenarioService} instance. - * - * @param accountingService The {@link UserAccountingService} instance to use. - */ - public ScenarioService(UserAccountingService accountingService) { - this.accountingService = accountingService; - } - - /** - * List all {@link Scenario}s that belong a certain portfolio. - */ - public List<org.opendc.web.proto.user.Scenario> findAll(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Scenario.findByPortfolio(projectId, number).stream() - .map((s) -> toUserDto(s, auth)) - .toList(); - } - - /** - * Obtain a {@link Scenario} by identifier. - */ - public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Scenario scenario = Scenario.findByProject(projectId, number); - - if (scenario == null) { - return null; - } - - return toUserDto(scenario, auth); - } - - /** - * Delete the specified scenario. - */ - public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Scenario entity = Scenario.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Scenario} with the specified data. - */ - public org.opendc.web.proto.user.Scenario create( - String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); - - if (portfolio == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); - if (topology == null) { - throw new IllegalArgumentException("Referred topology does not exist"); - } - - Trace trace = Trace.findById(request.getWorkload().getTrace()); - if (trace == null) { - throw new IllegalArgumentException("Referred trace does not exist"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocateScenario(now); - - Scenario scenario = new Scenario( - project, - portfolio, - number, - request.getName(), - new Workload(trace, request.getWorkload().getSamplingFraction()), - topology, - request.getPhenomena(), - request.getSchedulerName()); - Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); - - // Fail the job if there is not enough budget for the simulation - if (!accountingService.hasSimulationBudget(userId)) { - job.state = JobState.FAILED; - } - - scenario.job = job; - portfolio.scenarios.add(scenario); - scenario.persist(); - - return toUserDto(scenario, auth); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. - */ - public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Scenario( - scenario.id, - scenario.number, - ProjectService.toUserDto(auth), - PortfolioService.toSummaryDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. - */ - public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { - return new org.opendc.web.proto.user.Scenario.Summary( - scenario.id, - scenario.number, - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) { - return new org.opendc.web.proto.runner.Scenario( - scenario.id, - scenario.number, - PortfolioService.toRunnerDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toRunnerDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName); - } - - /** - * Convert a {@link Workload} entity into a DTO. - */ - public static org.opendc.web.proto.Workload toDto(Workload workload) { - return new org.opendc.web.proto.Workload(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/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java deleted file mode 100644 index 1961995f..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2023 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.web.server.service; - -import java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Topology; - -/** - * Service for managing {@link Topology}s. - */ -@ApplicationScoped -public final class TopologyService { - /** - * List all {@link Topology}s that belong a certain project. - */ - public List<org.opendc.web.proto.user.Topology> findAll(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Topology.findByProject(projectId).stream() - .map((t) -> toUserDto(t, auth)) - .toList(); - } - - /** - * Find the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, number); - - if (topology == null) { - return null; - } - - return toUserDto(topology, auth); - } - - /** - * Delete the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code> - */ - public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Update a {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>. - */ - public org.opendc.web.proto.user.Topology update( - String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.rooms = request.getRooms(); - - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Topology} with the specified name. - */ - public org.opendc.web.proto.user.Topology create( - String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Instant now = Instant.now(); - Project project = auth.project; - int number = project.allocateTopology(now); - - Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); - - project.topologies.add(topology); - topology.persist(); - - return toUserDto(topology, auth); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. - */ - public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Topology( - topology.id, - topology.number, - ProjectService.toUserDto(auth), - topology.name, - topology.rooms, - topology.createdAt, - topology.updatedAt); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. - */ - public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { - return new org.opendc.web.proto.user.Topology.Summary( - topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); - } - - /** - * Convert a {@link Topology} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Topology toRunnerDto(Topology topology) { - return new org.opendc.web.proto.runner.Topology( - topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java index 84ebd6e4..0331eacf 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -28,17 +28,26 @@ import javax.transaction.Transactional; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.opendc.web.proto.JobState; -import org.opendc.web.proto.runner.Job; 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 {@link JobService} without overhead of the REST API. + * 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; } @@ -46,38 +55,60 @@ public class QuarkusJobManager implements JobManager { @Transactional @Nullable @Override - public Job findNext() { - var pending = jobService.listPending(); - if (pending.isEmpty()) { + public org.opendc.web.proto.runner.Job findNext() { + var job = Job.findByState(JobState.PENDING).firstResult(); + if (job == null) { return null; } - return pending.get(0); + return RunnerProtocol.toDto(job); } + @Transactional @Override public boolean claim(long id) { - try { - jobService.updateState(id, JobState.CLAIMED, 0, null); - return true; - } catch (IllegalStateException e) { - return false; - } + return updateState(id, JobState.CLAIMED, 0, null); } + @Transactional @Override public boolean heartbeat(long id, int runtime) { - Job res = jobService.updateState(id, JobState.RUNNING, runtime, null); - return res != null && !res.getState().equals(JobState.FAILED); + return updateState(id, JobState.RUNNING, runtime, null); } + @Transactional @Override public void fail(long id, int runtime) { - jobService.updateState(id, JobState.FAILED, runtime, null); + updateState(id, JobState.FAILED, runtime, null); } + @Transactional @Override public void finish(long id, int runtime, @NotNull Map<String, ?> results) { - jobService.updateState(id, JobState.FINISHED, runtime, 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; + } } } diff --git a/opendc-web/opendc-web-server/src/main/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties index 17502b6c..bee17221 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application-test.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application-test.properties @@ -25,6 +25,7 @@ quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TY quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect quarkus.hibernate-orm.log.sql=true quarkus.flyway.clean-at-start=true +quarkus.flyway.locations=db/migration,db/testing # Disable security quarkus.oidc.enabled=false diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties index 40933304..0f47db30 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application.properties @@ -20,6 +20,7 @@ # Enable CORS quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000,https://opendc.org # Security quarkus.oidc.enabled=${opendc.security.enabled} diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql index 1a0e4046..40654b6b 100644 --- a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql +++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql @@ -7,19 +7,21 @@ create table projects id bigint not null, created_at timestamp not null, name varchar(255) not null, - portfolios_created integer not null, - scenarios_created integer not null, - topologies_created integer not null, + portfolios_created integer not null default 0, + scenarios_created integer not null default 0, + topologies_created integer not null default 0, updated_at timestamp not null, primary key (id) ); +create type project_role as enum ('OWNER', 'EDITOR', 'VIEWER'); + -- Project authorizations authorize users specific permissions to a project. create table project_authorizations ( project_id bigint not null, user_id varchar(255) not null, - role integer not null, + role project_role not null, primary key (project_id, user_id) ); @@ -55,7 +57,6 @@ create table scenarios phenomena jsonb not null, scheduler_name varchar(255) not null, sampling_fraction double precision not null, - job_id bigint, portfolio_id bigint not null, project_id bigint not null, topology_id bigint not null, @@ -63,16 +64,19 @@ create table scenarios primary key (id) ); +create type job_state as enum ('PENDING', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED'); + create table jobs ( - id bigint not null, - created_by varchar(255) not null, - created_at timestamp not null, - repeats integer not null, - results jsonb, - state integer not null, - runtime integer not null, - updated_at timestamp not null, + id bigint not null, + created_by varchar(255) not null, + created_at timestamp not null, + repeats integer not null, + results jsonb, + state job_state not null default 'PENDING', + runtime integer not null default 0, + updated_at timestamp not null, + scenario_id bigint not null, primary key (id) ); @@ -97,60 +101,60 @@ create table traces -- Relations alter table project_authorizations - add constraint FK824hw0npe6gwiamwb6vohsu19 + add constraint fk_project_authorizations foreign key (project_id) references projects; -create index fn_topologies_number on topologies (project_id, number); +create index ux_topologies_number on topologies (project_id, number); alter table topologies - add constraint UK2s5na63qtu2of4g7odocmwi2a unique (project_id, number); + add constraint uk_topologies_number unique (project_id, number); alter table topologies - add constraint FK1kpw87pylq7m2ct9lq0ed1u3b + add constraint fk_topologies_project foreign key (project_id) references projects; -create index fn_portfolios_number on portfolios (project_id, number); +create index ux_portfolios_number on portfolios (project_id, number); alter table portfolios - add constraint FK31ytuaxb7aboxueng9hq7owwa + add constraint fk_portfolios_project foreign key (project_id) references projects; alter table portfolios - add constraint UK56dtskxruwj22dvxny2hfhks1 unique (project_id, number); - -create index fn_scenarios_number on scenarios (project_id, number); - -alter table scenarios - add constraint UKd0bk6fmtw5qiu9ty7t3g9crqd unique (project_id, number); + add constraint uk_portfolios_number unique (project_id, number); -alter table scenarios - add constraint FK9utvg0i5uu8db9pa17a1d77iy - foreign key (job_id) - references jobs; +create index ux_scenarios_number on scenarios (project_id, number); alter table scenarios - add constraint FK181y5hv0uibhj7fpbpkdy90s5 - foreign key (portfolio_id) - references portfolios; + add constraint uk_scenarios_number unique (project_id, number); alter table scenarios - add constraint FKbvwyh4joavs444rj270o3b8fr + add constraint fk_scenarios_project foreign key (project_id) references projects; alter table scenarios - add constraint FKrk6ltvaf9lp0aukp9dq3qjujj + add constraint fk_scenarios_topology foreign key (topology_id) references topologies; alter table scenarios - add constraint FK5m05tqeekqjkbbsaj3ehl6o8n + add constraint fk_scenarios_portfolio + foreign key (portfolio_id) + references portfolios; + +alter table scenarios + add constraint fk_scenarios_trace foreign key (trace_id) references traces; +alter table jobs + add constraint fk_scenarios_job + foreign key (scenario_id) + references scenarios; + -- Initial data insert into traces (id, name, type) values ('bitbrains-small', 'Bitbrains Small', 'vm'); diff --git a/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql new file mode 100644 index 00000000..1b702f4e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql @@ -0,0 +1,24 @@ +-- Test entities + +alter sequence hibernate_sequence restart with 500; + +insert into projects (id, created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at) +values (1, current_timestamp(), 'Test Project', 1, 2, 1, current_timestamp()); +insert into project_authorizations (project_id, user_id, role) +values (1, 'owner', 'OWNER'), + (1, 'editor', 'EDITOR'), + (1, 'viewer', 'VIEWER'); + +insert into portfolios (id, name, number, targets, project_id) +values (1, 'Test Portfolio', 1, '{ "metrics": [] }' format json, 1); + +insert into topologies (id, created_at, name, number, rooms, updated_at, project_id) +values (1, current_timestamp(), 'Test Topology', 1, '[]' format json, current_timestamp(), 1); + +insert into scenarios (id, name, number, phenomena, scheduler_name, sampling_fraction, portfolio_id, project_id, topology_id, trace_id) +values (1, 'Test Scenario', 1, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'), + (2, 'Test Scenario', 2, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'); + +insert into jobs (id, created_by, created_at, repeats, updated_at, scenario_id) +values (1, 'owner', current_timestamp(), 1, current_timestamp(), 1), + (2, 'owner', current_timestamp(), 1, current_timestamp(), 2); |
