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));
+    }
+}