You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by di...@apache.org on 2026/08/07 04:21:15 UTC
(airavata) branch refactor-single-module updated: Slurm cluster management data models and APIs
This is an automated email from the ASF dual-hosted git repository.
DImuthuUpe pushed a commit to branch refactor-single-module
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/refactor-single-module by this push:
new 630c406649 Slurm cluster management data models and APIs
630c406649 is described below
commit 630c4066497b38dd287a416dc46f12c19e57a295
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Fri Aug 7 00:21:10 2026 -0400
Slurm cluster management data models and APIs
---
.../airavata/api/SlurmClusterController.java | 57 ++++++
.../airavata/api/SlurmPartitionController.java | 60 ++++++
.../compute/dto/SlurmClusterRequestDto.java | 54 ++++++
.../compute/dto/SlurmClusterResponseDto.java | 62 +++++++
.../compute/dto/SlurmPartitionRequestDto.java | 150 +++++++++++++++
.../compute/dto/SlurmPartitionResponseDto.java | 159 ++++++++++++++++
.../compute/mapper/SlurmClusterMapper.java | 30 +++
.../compute/mapper/SlurmPartitionMapper.java | 28 +++
.../airavata/compute/model/SlurmClusterEntity.java | 81 ++++++++
.../compute/model/SlurmPartitionEntity.java | 204 +++++++++++++++++++++
.../compute/repository/SlurmClusterRepository.java | 11 ++
.../repository/SlurmPartitionRepository.java | 19 ++
.../compute/service/SlurmClusterService.java | 71 +++++++
.../compute/service/SlurmPartitionService.java | 93 ++++++++++
14 files changed, 1079 insertions(+)
diff --git a/src/main/java/org/apache/airavata/api/SlurmClusterController.java b/src/main/java/org/apache/airavata/api/SlurmClusterController.java
new file mode 100644
index 0000000000..936050b878
--- /dev/null
+++ b/src/main/java/org/apache/airavata/api/SlurmClusterController.java
@@ -0,0 +1,57 @@
+package org.apache.airavata.api;
+
+import jakarta.validation.Valid;
+import java.util.List;
+import org.apache.airavata.compute.dto.SlurmClusterRequestDto;
+import org.apache.airavata.compute.dto.SlurmClusterResponseDto;
+import org.apache.airavata.compute.service.SlurmClusterService;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/slurm-clusters")
+public class SlurmClusterController {
+
+ private final SlurmClusterService slurmClusterService;
+
+ public SlurmClusterController(SlurmClusterService slurmClusterService) {
+ this.slurmClusterService = slurmClusterService;
+ }
+
+ @GetMapping
+ public List<SlurmClusterResponseDto> getAllClusters() {
+ return slurmClusterService.getAllClusters();
+ }
+
+ @GetMapping("/{clusterId}")
+ public SlurmClusterResponseDto getCluster(@PathVariable String clusterId) {
+ return slurmClusterService.getCluster(clusterId);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public SlurmClusterResponseDto createCluster(@Valid @RequestBody SlurmClusterRequestDto request) {
+ return slurmClusterService.createCluster(request);
+ }
+
+ @PutMapping("/{clusterId}")
+ public SlurmClusterResponseDto updateCluster(
+ @PathVariable String clusterId, @Valid @RequestBody SlurmClusterRequestDto request) {
+ return slurmClusterService.updateCluster(clusterId, request);
+ }
+
+ /** Deleting a cluster also deletes its partitions. */
+ @DeleteMapping("/{clusterId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteCluster(@PathVariable String clusterId) {
+ slurmClusterService.deleteCluster(clusterId);
+ }
+}
diff --git a/src/main/java/org/apache/airavata/api/SlurmPartitionController.java b/src/main/java/org/apache/airavata/api/SlurmPartitionController.java
new file mode 100644
index 0000000000..87e27a5471
--- /dev/null
+++ b/src/main/java/org/apache/airavata/api/SlurmPartitionController.java
@@ -0,0 +1,60 @@
+package org.apache.airavata.api;
+
+import jakarta.validation.Valid;
+import java.util.List;
+import org.apache.airavata.compute.dto.SlurmPartitionRequestDto;
+import org.apache.airavata.compute.dto.SlurmPartitionResponseDto;
+import org.apache.airavata.compute.service.SlurmPartitionService;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+/** Partitions are a sub-resource of their cluster; every path is scoped by cluster id. */
+@RestController
+@RequestMapping("/api/v1/slurm-clusters/{clusterId}/partitions")
+public class SlurmPartitionController {
+
+ private final SlurmPartitionService slurmPartitionService;
+
+ public SlurmPartitionController(SlurmPartitionService slurmPartitionService) {
+ this.slurmPartitionService = slurmPartitionService;
+ }
+
+ @GetMapping
+ public List<SlurmPartitionResponseDto> getPartitions(@PathVariable String clusterId) {
+ return slurmPartitionService.getPartitions(clusterId);
+ }
+
+ @GetMapping("/{partitionId}")
+ public SlurmPartitionResponseDto getPartition(@PathVariable String clusterId, @PathVariable String partitionId) {
+ return slurmPartitionService.getPartition(clusterId, partitionId);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public SlurmPartitionResponseDto createPartition(
+ @PathVariable String clusterId, @Valid @RequestBody SlurmPartitionRequestDto request) {
+ return slurmPartitionService.createPartition(clusterId, request);
+ }
+
+ @PutMapping("/{partitionId}")
+ public SlurmPartitionResponseDto updatePartition(
+ @PathVariable String clusterId,
+ @PathVariable String partitionId,
+ @Valid @RequestBody SlurmPartitionRequestDto request) {
+ return slurmPartitionService.updatePartition(clusterId, partitionId, request);
+ }
+
+ @DeleteMapping("/{partitionId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deletePartition(@PathVariable String clusterId, @PathVariable String partitionId) {
+ slurmPartitionService.deletePartition(clusterId, partitionId);
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/dto/SlurmClusterRequestDto.java b/src/main/java/org/apache/airavata/compute/dto/SlurmClusterRequestDto.java
new file mode 100644
index 0000000000..55bb969de4
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/dto/SlurmClusterRequestDto.java
@@ -0,0 +1,54 @@
+package org.apache.airavata.compute.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ * Create/update payload for a Slurm cluster. Partitions are not accepted here — they are
+ * managed through {@code /api/v1/slurm-clusters/{clusterId}/partitions} so that updating
+ * a cluster cannot silently drop them.
+ */
+public class SlurmClusterRequestDto {
+
+ @NotBlank(message = "Cluster name cannot be blank")
+ private String clusterName;
+
+ private String clusterDescription;
+
+ @NotBlank(message = "Host name cannot be blank")
+ private String hostName;
+
+ @NotBlank(message = "Slurm home cannot be blank")
+ private String slurmHome;
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public String getClusterDescription() {
+ return clusterDescription;
+ }
+
+ public void setClusterDescription(String clusterDescription) {
+ this.clusterDescription = clusterDescription;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public void setHostName(String hostName) {
+ this.hostName = hostName;
+ }
+
+ public String getSlurmHome() {
+ return slurmHome;
+ }
+
+ public void setSlurmHome(String slurmHome) {
+ this.slurmHome = slurmHome;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/dto/SlurmClusterResponseDto.java b/src/main/java/org/apache/airavata/compute/dto/SlurmClusterResponseDto.java
new file mode 100644
index 0000000000..3a1834ea8e
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/dto/SlurmClusterResponseDto.java
@@ -0,0 +1,62 @@
+package org.apache.airavata.compute.dto;
+
+import java.util.List;
+
+/** Read view of a Slurm cluster, including its partitions. */
+public class SlurmClusterResponseDto {
+
+ private String clusterId;
+ private String clusterName;
+ private String clusterDescription;
+ private String hostName;
+ private String slurmHome;
+ private List<SlurmPartitionResponseDto> partitions;
+
+ public String getClusterId() {
+ return clusterId;
+ }
+
+ public void setClusterId(String clusterId) {
+ this.clusterId = clusterId;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public String getClusterDescription() {
+ return clusterDescription;
+ }
+
+ public void setClusterDescription(String clusterDescription) {
+ this.clusterDescription = clusterDescription;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public void setHostName(String hostName) {
+ this.hostName = hostName;
+ }
+
+ public String getSlurmHome() {
+ return slurmHome;
+ }
+
+ public void setSlurmHome(String slurmHome) {
+ this.slurmHome = slurmHome;
+ }
+
+ public List<SlurmPartitionResponseDto> getPartitions() {
+ return partitions;
+ }
+
+ public void setPartitions(List<SlurmPartitionResponseDto> partitions) {
+ this.partitions = partitions;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionRequestDto.java b/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionRequestDto.java
new file mode 100644
index 0000000000..bf304b5a15
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionRequestDto.java
@@ -0,0 +1,150 @@
+package org.apache.airavata.compute.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+/** Create/update payload for a partition of a Slurm cluster. */
+public class SlurmPartitionRequestDto {
+
+ @NotBlank(message = "Partition name cannot be blank")
+ private String name;
+
+ private String description;
+ private Integer maxRunTime;
+ private Integer maxNodes;
+ private Integer maxProcessors;
+ private Integer maxJobsInQueue;
+ private Long maxMemory;
+ private Integer cpuPerNode;
+ private Integer defaultNodeCount;
+ private Integer defaultCpuCount;
+ private Long defaultWalltime;
+
+ /** Comma-separated list of generic resources (GRES) associated with the partition. */
+ private String gres;
+
+ /** Comma-separated list of nodes associated with the partition. */
+ private String nodes;
+
+ private Boolean isDefaultQueue;
+ private Boolean isCheckpointable;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Integer getMaxRunTime() {
+ return maxRunTime;
+ }
+
+ public void setMaxRunTime(Integer maxRunTime) {
+ this.maxRunTime = maxRunTime;
+ }
+
+ public Integer getMaxNodes() {
+ return maxNodes;
+ }
+
+ public void setMaxNodes(Integer maxNodes) {
+ this.maxNodes = maxNodes;
+ }
+
+ public Integer getMaxProcessors() {
+ return maxProcessors;
+ }
+
+ public void setMaxProcessors(Integer maxProcessors) {
+ this.maxProcessors = maxProcessors;
+ }
+
+ public Integer getMaxJobsInQueue() {
+ return maxJobsInQueue;
+ }
+
+ public void setMaxJobsInQueue(Integer maxJobsInQueue) {
+ this.maxJobsInQueue = maxJobsInQueue;
+ }
+
+ public Long getMaxMemory() {
+ return maxMemory;
+ }
+
+ public void setMaxMemory(Long maxMemory) {
+ this.maxMemory = maxMemory;
+ }
+
+ public Integer getCpuPerNode() {
+ return cpuPerNode;
+ }
+
+ public void setCpuPerNode(Integer cpuPerNode) {
+ this.cpuPerNode = cpuPerNode;
+ }
+
+ public Integer getDefaultNodeCount() {
+ return defaultNodeCount;
+ }
+
+ public void setDefaultNodeCount(Integer defaultNodeCount) {
+ this.defaultNodeCount = defaultNodeCount;
+ }
+
+ public Integer getDefaultCpuCount() {
+ return defaultCpuCount;
+ }
+
+ public void setDefaultCpuCount(Integer defaultCpuCount) {
+ this.defaultCpuCount = defaultCpuCount;
+ }
+
+ public Long getDefaultWalltime() {
+ return defaultWalltime;
+ }
+
+ public void setDefaultWalltime(Long defaultWalltime) {
+ this.defaultWalltime = defaultWalltime;
+ }
+
+ public String getGres() {
+ return gres;
+ }
+
+ public void setGres(String gres) {
+ this.gres = gres;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public Boolean getIsDefaultQueue() {
+ return isDefaultQueue;
+ }
+
+ public void setIsDefaultQueue(Boolean isDefaultQueue) {
+ this.isDefaultQueue = isDefaultQueue;
+ }
+
+ public Boolean getIsCheckpointable() {
+ return isCheckpointable;
+ }
+
+ public void setIsCheckpointable(Boolean isCheckpointable) {
+ this.isCheckpointable = isCheckpointable;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionResponseDto.java b/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionResponseDto.java
new file mode 100644
index 0000000000..eeb81197b8
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/dto/SlurmPartitionResponseDto.java
@@ -0,0 +1,159 @@
+package org.apache.airavata.compute.dto;
+
+/** Read view of a partition; the owning cluster is referenced by id only. */
+public class SlurmPartitionResponseDto {
+
+ private String partitionId;
+ private String clusterId;
+ private String name;
+ private String description;
+ private Integer maxRunTime;
+ private Integer maxNodes;
+ private Integer maxProcessors;
+ private Integer maxJobsInQueue;
+ private Long maxMemory;
+ private Integer cpuPerNode;
+ private Integer defaultNodeCount;
+ private Integer defaultCpuCount;
+ private Long defaultWalltime;
+ private String gres;
+ private String nodes;
+ private Boolean isDefaultQueue;
+ private Boolean isCheckpointable;
+
+ public String getPartitionId() {
+ return partitionId;
+ }
+
+ public void setPartitionId(String partitionId) {
+ this.partitionId = partitionId;
+ }
+
+ public String getClusterId() {
+ return clusterId;
+ }
+
+ public void setClusterId(String clusterId) {
+ this.clusterId = clusterId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Integer getMaxRunTime() {
+ return maxRunTime;
+ }
+
+ public void setMaxRunTime(Integer maxRunTime) {
+ this.maxRunTime = maxRunTime;
+ }
+
+ public Integer getMaxNodes() {
+ return maxNodes;
+ }
+
+ public void setMaxNodes(Integer maxNodes) {
+ this.maxNodes = maxNodes;
+ }
+
+ public Integer getMaxProcessors() {
+ return maxProcessors;
+ }
+
+ public void setMaxProcessors(Integer maxProcessors) {
+ this.maxProcessors = maxProcessors;
+ }
+
+ public Integer getMaxJobsInQueue() {
+ return maxJobsInQueue;
+ }
+
+ public void setMaxJobsInQueue(Integer maxJobsInQueue) {
+ this.maxJobsInQueue = maxJobsInQueue;
+ }
+
+ public Long getMaxMemory() {
+ return maxMemory;
+ }
+
+ public void setMaxMemory(Long maxMemory) {
+ this.maxMemory = maxMemory;
+ }
+
+ public Integer getCpuPerNode() {
+ return cpuPerNode;
+ }
+
+ public void setCpuPerNode(Integer cpuPerNode) {
+ this.cpuPerNode = cpuPerNode;
+ }
+
+ public Integer getDefaultNodeCount() {
+ return defaultNodeCount;
+ }
+
+ public void setDefaultNodeCount(Integer defaultNodeCount) {
+ this.defaultNodeCount = defaultNodeCount;
+ }
+
+ public Integer getDefaultCpuCount() {
+ return defaultCpuCount;
+ }
+
+ public void setDefaultCpuCount(Integer defaultCpuCount) {
+ this.defaultCpuCount = defaultCpuCount;
+ }
+
+ public Long getDefaultWalltime() {
+ return defaultWalltime;
+ }
+
+ public void setDefaultWalltime(Long defaultWalltime) {
+ this.defaultWalltime = defaultWalltime;
+ }
+
+ public String getGres() {
+ return gres;
+ }
+
+ public void setGres(String gres) {
+ this.gres = gres;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public Boolean getIsDefaultQueue() {
+ return isDefaultQueue;
+ }
+
+ public void setIsDefaultQueue(Boolean isDefaultQueue) {
+ this.isDefaultQueue = isDefaultQueue;
+ }
+
+ public Boolean getIsCheckpointable() {
+ return isCheckpointable;
+ }
+
+ public void setIsCheckpointable(Boolean isCheckpointable) {
+ this.isCheckpointable = isCheckpointable;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/mapper/SlurmClusterMapper.java b/src/main/java/org/apache/airavata/compute/mapper/SlurmClusterMapper.java
new file mode 100644
index 0000000000..15d2d441e9
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/mapper/SlurmClusterMapper.java
@@ -0,0 +1,30 @@
+package org.apache.airavata.compute.mapper;
+
+import org.apache.airavata.compute.dto.SlurmClusterRequestDto;
+import org.apache.airavata.compute.dto.SlurmClusterResponseDto;
+import org.apache.airavata.compute.model.SlurmClusterEntity;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * Entity/DTO conversions for Slurm clusters. Partitions are read-only on this side:
+ * they are mapped out to the response but never written from a cluster request.
+ */
+@Mapper(
+ componentModel = "spring",
+ uses = SlurmPartitionMapper.class,
+ unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface SlurmClusterMapper {
+
+ SlurmClusterResponseDto toResponseDto(SlurmClusterEntity entity);
+
+ @Mapping(target = "clusterId", ignore = true)
+ @Mapping(target = "partitions", ignore = true)
+ SlurmClusterEntity toEntity(SlurmClusterRequestDto dto);
+
+ @Mapping(target = "clusterId", ignore = true)
+ @Mapping(target = "partitions", ignore = true)
+ void updateEntity(SlurmClusterRequestDto dto, @MappingTarget SlurmClusterEntity entity);
+}
diff --git a/src/main/java/org/apache/airavata/compute/mapper/SlurmPartitionMapper.java b/src/main/java/org/apache/airavata/compute/mapper/SlurmPartitionMapper.java
new file mode 100644
index 0000000000..97f4968f1e
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/mapper/SlurmPartitionMapper.java
@@ -0,0 +1,28 @@
+package org.apache.airavata.compute.mapper;
+
+import org.apache.airavata.compute.dto.SlurmPartitionRequestDto;
+import org.apache.airavata.compute.dto.SlurmPartitionResponseDto;
+import org.apache.airavata.compute.model.SlurmPartitionEntity;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * Entity/DTO conversions for Slurm partitions. The owning cluster is resolved by the
+ * service from the path, so it is never mapped off the request body.
+ */
+@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface SlurmPartitionMapper {
+
+ @Mapping(target = "clusterId", source = "slurmCluster.clusterId")
+ SlurmPartitionResponseDto toResponseDto(SlurmPartitionEntity entity);
+
+ @Mapping(target = "partitionId", ignore = true)
+ @Mapping(target = "slurmCluster", ignore = true)
+ SlurmPartitionEntity toEntity(SlurmPartitionRequestDto dto);
+
+ @Mapping(target = "partitionId", ignore = true)
+ @Mapping(target = "slurmCluster", ignore = true)
+ void updateEntity(SlurmPartitionRequestDto dto, @MappingTarget SlurmPartitionEntity entity);
+}
diff --git a/src/main/java/org/apache/airavata/compute/model/SlurmClusterEntity.java b/src/main/java/org/apache/airavata/compute/model/SlurmClusterEntity.java
new file mode 100644
index 0000000000..5bd33ea688
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/model/SlurmClusterEntity.java
@@ -0,0 +1,81 @@
+package org.apache.airavata.compute.model;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+public class SlurmClusterEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private String clusterId;
+
+ @Column(nullable = false)
+ private String clusterName;
+ @Column(nullable = true)
+ private String clusterDescription;
+ @Column(nullable = false)
+ private String hostName;
+ @Column(nullable = false)
+ private String slurmHome;
+
+ // Partitions are owned by the cluster: they are created, replaced and deleted
+ // with it, and have no meaning outside of it.
+ @OneToMany(mappedBy = "slurmCluster", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List<SlurmPartitionEntity> partitions = new ArrayList<>();
+
+ public String getClusterId() {
+ return clusterId;
+ }
+
+ public void setClusterId(String clusterId) {
+ this.clusterId = clusterId;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public String getClusterDescription() {
+ return clusterDescription;
+ }
+
+ public void setClusterDescription(String clusterDescription) {
+ this.clusterDescription = clusterDescription;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public void setHostName(String hostName) {
+ this.hostName = hostName;
+ }
+
+ public String getSlurmHome() {
+ return slurmHome;
+ }
+
+ public void setSlurmHome(String slurmHome) {
+ this.slurmHome = slurmHome;
+ }
+
+ public List<SlurmPartitionEntity> getPartitions() {
+ return partitions;
+ }
+
+ public void setPartitions(List<SlurmPartitionEntity> partitions) {
+ this.partitions = partitions;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/model/SlurmPartitionEntity.java b/src/main/java/org/apache/airavata/compute/model/SlurmPartitionEntity.java
new file mode 100644
index 0000000000..3987f03ba1
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/model/SlurmPartitionEntity.java
@@ -0,0 +1,204 @@
+package org.apache.airavata.compute.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.ForeignKey;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+
+@Entity
+public class SlurmPartitionEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private String partitionId;
+
+ @ManyToOne
+ @JoinColumn(name = "cluster_id", foreignKey = @ForeignKey(name = "fk_partition_cluster"))
+ private SlurmClusterEntity slurmCluster;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = true)
+ private String description;
+
+ @Column(nullable = true)
+ private Integer maxRunTime;
+
+ @Column(nullable = true)
+ private Integer maxNodes;
+
+ @Column(nullable = true)
+ private Integer maxProcessors;
+
+ @Column(nullable = true)
+ private Integer maxJobsInQueue;
+
+ @Column(nullable = true)
+ private Long maxMemory;
+
+ @Column(nullable = true)
+ private Integer cpuPerNode;
+
+ @Column(nullable = true)
+ private Integer defaultNodeCount;
+
+ @Column(nullable = true)
+ private Integer defaultCpuCount;
+
+ @Column(nullable = true)
+ private Long defaultWalltime;
+
+ @Column(nullable = true)
+ private String gres; // Comma-separated list of generic resources (GRES) associated with the
+ // partition
+
+ @Column(nullable = true)
+ private String nodes; // Comma-separated list of nodes associated with the partition
+
+ @Column(nullable = true)
+ private Boolean isDefaultQueue;
+
+ @Column(nullable = true)
+ private Boolean isCheckpointable;
+
+ public String getPartitionId() {
+ return partitionId;
+ }
+
+ public void setPartitionId(String partitionId) {
+ this.partitionId = partitionId;
+ }
+
+ public SlurmClusterEntity getSlurmCluster() {
+ return slurmCluster;
+ }
+
+ public void setSlurmCluster(SlurmClusterEntity slurmCluster) {
+ this.slurmCluster = slurmCluster;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Integer getMaxRunTime() {
+ return maxRunTime;
+ }
+
+ public void setMaxRunTime(Integer maxRunTime) {
+ this.maxRunTime = maxRunTime;
+ }
+
+ public Integer getMaxNodes() {
+ return maxNodes;
+ }
+
+ public void setMaxNodes(Integer maxNodes) {
+ this.maxNodes = maxNodes;
+ }
+
+ public Integer getMaxProcessors() {
+ return maxProcessors;
+ }
+
+ public void setMaxProcessors(Integer maxProcessors) {
+ this.maxProcessors = maxProcessors;
+ }
+
+ public Integer getMaxJobsInQueue() {
+ return maxJobsInQueue;
+ }
+
+ public void setMaxJobsInQueue(Integer maxJobsInQueue) {
+ this.maxJobsInQueue = maxJobsInQueue;
+ }
+
+ public Long getMaxMemory() {
+ return maxMemory;
+ }
+
+ public void setMaxMemory(Long maxMemory) {
+ this.maxMemory = maxMemory;
+ }
+
+ public Integer getCpuPerNode() {
+ return cpuPerNode;
+ }
+
+ public void setCpuPerNode(Integer cpuPerNode) {
+ this.cpuPerNode = cpuPerNode;
+ }
+
+ public Integer getDefaultNodeCount() {
+ return defaultNodeCount;
+ }
+
+ public void setDefaultNodeCount(Integer defaultNodeCount) {
+ this.defaultNodeCount = defaultNodeCount;
+ }
+
+ public Integer getDefaultCpuCount() {
+ return defaultCpuCount;
+ }
+
+ public void setDefaultCpuCount(Integer defaultCpuCount) {
+ this.defaultCpuCount = defaultCpuCount;
+ }
+
+ public Long getDefaultWalltime() {
+ return defaultWalltime;
+ }
+
+ public void setDefaultWalltime(Long defaultWalltime) {
+ this.defaultWalltime = defaultWalltime;
+ }
+
+ public String getGres() {
+ return gres;
+ }
+
+ public void setGres(String gres) {
+ this.gres = gres;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public Boolean getIsDefaultQueue() {
+ return isDefaultQueue;
+ }
+
+ public void setIsDefaultQueue(Boolean isDefaultQueue) {
+ this.isDefaultQueue = isDefaultQueue;
+ }
+
+ public Boolean getIsCheckpointable() {
+ return isCheckpointable;
+ }
+
+ public void setIsCheckpointable(Boolean isCheckpointable) {
+ this.isCheckpointable = isCheckpointable;
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/repository/SlurmClusterRepository.java b/src/main/java/org/apache/airavata/compute/repository/SlurmClusterRepository.java
new file mode 100644
index 0000000000..f551ce6dc6
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/repository/SlurmClusterRepository.java
@@ -0,0 +1,11 @@
+package org.apache.airavata.compute.repository;
+
+import org.apache.airavata.compute.model.SlurmClusterEntity;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface SlurmClusterRepository extends ListCrudRepository<SlurmClusterEntity, String> {
+
+ boolean existsByClusterName(String clusterName);
+}
diff --git a/src/main/java/org/apache/airavata/compute/repository/SlurmPartitionRepository.java b/src/main/java/org/apache/airavata/compute/repository/SlurmPartitionRepository.java
new file mode 100644
index 0000000000..4d6d771075
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/repository/SlurmPartitionRepository.java
@@ -0,0 +1,19 @@
+package org.apache.airavata.compute.repository;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.airavata.compute.model.SlurmPartitionEntity;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface SlurmPartitionRepository extends ListCrudRepository<SlurmPartitionEntity, String> {
+
+ List<SlurmPartitionEntity> findBySlurmCluster_ClusterId(String clusterId);
+
+ /**
+ * Scopes a partition lookup to its cluster so a partition id from one cluster cannot
+ * be read or mutated through another cluster's sub-resource path.
+ */
+ Optional<SlurmPartitionEntity> findByPartitionIdAndSlurmCluster_ClusterId(String partitionId, String clusterId);
+}
diff --git a/src/main/java/org/apache/airavata/compute/service/SlurmClusterService.java b/src/main/java/org/apache/airavata/compute/service/SlurmClusterService.java
new file mode 100644
index 0000000000..34c1eaeb67
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/service/SlurmClusterService.java
@@ -0,0 +1,71 @@
+package org.apache.airavata.compute.service;
+
+import java.util.List;
+import org.apache.airavata.compute.dto.SlurmClusterRequestDto;
+import org.apache.airavata.compute.dto.SlurmClusterResponseDto;
+import org.apache.airavata.compute.mapper.SlurmClusterMapper;
+import org.apache.airavata.compute.model.SlurmClusterEntity;
+import org.apache.airavata.compute.repository.SlurmClusterRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+/**
+ * CRUD for Slurm clusters.
+ *
+ * <p>Reads run inside a transaction so the lazily loaded partition collection can be
+ * mapped to DTOs before the session closes — {@code spring.jpa.open-in-view=false} means
+ * it cannot be walked from the serialization layer.
+ *
+ * <p>Deleting a cluster cascades to its partitions, which are owned by it.
+ */
+@Service
+public class SlurmClusterService {
+
+ private final SlurmClusterRepository clusterRepository;
+ private final SlurmClusterMapper mapper;
+
+ public SlurmClusterService(SlurmClusterRepository clusterRepository, SlurmClusterMapper mapper) {
+ this.clusterRepository = clusterRepository;
+ this.mapper = mapper;
+ }
+
+ @Transactional(readOnly = true)
+ public List<SlurmClusterResponseDto> getAllClusters() {
+ return clusterRepository.findAll().stream().map(mapper::toResponseDto).toList();
+ }
+
+ @Transactional(readOnly = true)
+ public SlurmClusterResponseDto getCluster(String clusterId) {
+ return mapper.toResponseDto(findOrThrow(clusterId));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public SlurmClusterResponseDto createCluster(SlurmClusterRequestDto request) {
+ SlurmClusterEntity entity = mapper.toEntity(request);
+ return mapper.toResponseDto(clusterRepository.save(entity));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public SlurmClusterResponseDto updateCluster(String clusterId, SlurmClusterRequestDto request) {
+ SlurmClusterEntity entity = findOrThrow(clusterId);
+ mapper.updateEntity(request, entity);
+ return mapper.toResponseDto(clusterRepository.save(entity));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public void deleteCluster(String clusterId) {
+ clusterRepository.delete(findOrThrow(clusterId));
+ }
+
+ private SlurmClusterEntity findOrThrow(String clusterId) {
+ return clusterRepository
+ .findById(clusterId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Cluster not found: " + clusterId));
+ }
+}
diff --git a/src/main/java/org/apache/airavata/compute/service/SlurmPartitionService.java b/src/main/java/org/apache/airavata/compute/service/SlurmPartitionService.java
new file mode 100644
index 0000000000..bbc1c40df1
--- /dev/null
+++ b/src/main/java/org/apache/airavata/compute/service/SlurmPartitionService.java
@@ -0,0 +1,93 @@
+package org.apache.airavata.compute.service;
+
+import java.util.List;
+import org.apache.airavata.compute.dto.SlurmPartitionRequestDto;
+import org.apache.airavata.compute.dto.SlurmPartitionResponseDto;
+import org.apache.airavata.compute.mapper.SlurmPartitionMapper;
+import org.apache.airavata.compute.model.SlurmClusterEntity;
+import org.apache.airavata.compute.model.SlurmPartitionEntity;
+import org.apache.airavata.compute.repository.SlurmClusterRepository;
+import org.apache.airavata.compute.repository.SlurmPartitionRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+/**
+ * CRUD for the partitions of a Slurm cluster.
+ *
+ * <p>Every operation is scoped to a cluster id taken from the request path, so a
+ * partition belonging to one cluster is a 404 under another cluster's path rather than
+ * an accidental cross-cluster edit.
+ *
+ * <p>Partitions are persisted through their own repository rather than by mutating
+ * {@link SlurmClusterEntity#getPartitions()}: the association is orphan-removing, so
+ * touching that collection here would risk deleting rows this service never intended
+ * to remove.
+ */
+@Service
+public class SlurmPartitionService {
+
+ private final SlurmPartitionRepository partitionRepository;
+ private final SlurmClusterRepository clusterRepository;
+ private final SlurmPartitionMapper mapper;
+
+ public SlurmPartitionService(
+ SlurmPartitionRepository partitionRepository,
+ SlurmClusterRepository clusterRepository,
+ SlurmPartitionMapper mapper) {
+ this.partitionRepository = partitionRepository;
+ this.clusterRepository = clusterRepository;
+ this.mapper = mapper;
+ }
+
+ @Transactional(readOnly = true)
+ public List<SlurmPartitionResponseDto> getPartitions(String clusterId) {
+ requireCluster(clusterId);
+ return partitionRepository.findBySlurmCluster_ClusterId(clusterId).stream()
+ .map(mapper::toResponseDto)
+ .toList();
+ }
+
+ @Transactional(readOnly = true)
+ public SlurmPartitionResponseDto getPartition(String clusterId, String partitionId) {
+ return mapper.toResponseDto(findOrThrow(clusterId, partitionId));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public SlurmPartitionResponseDto createPartition(String clusterId, SlurmPartitionRequestDto request) {
+ SlurmPartitionEntity entity = mapper.toEntity(request);
+ entity.setSlurmCluster(requireCluster(clusterId));
+ return mapper.toResponseDto(partitionRepository.save(entity));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public SlurmPartitionResponseDto updatePartition(
+ String clusterId, String partitionId, SlurmPartitionRequestDto request) {
+ SlurmPartitionEntity entity = findOrThrow(clusterId, partitionId);
+ mapper.updateEntity(request, entity);
+ return mapper.toResponseDto(partitionRepository.save(entity));
+ }
+
+ @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+ @Transactional
+ public void deletePartition(String clusterId, String partitionId) {
+ partitionRepository.delete(findOrThrow(clusterId, partitionId));
+ }
+
+ private SlurmClusterEntity requireCluster(String clusterId) {
+ return clusterRepository
+ .findById(clusterId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Cluster not found: " + clusterId));
+ }
+
+ private SlurmPartitionEntity findOrThrow(String clusterId, String partitionId) {
+ return partitionRepository
+ .findByPartitionIdAndSlurmCluster_ClusterId(partitionId, clusterId)
+ .orElseThrow(() -> new ResponseStatusException(
+ HttpStatus.NOT_FOUND, "Partition not found: " + partitionId + " in cluster " + clusterId));
+ }
+}