Skip to content

Commit 9ab7ec5

Browse files
ONTAP: cache templates on primary storage and clone volumes from them
Enable the managed-storage template cache flow for the ONTAP primary storage plugin so that VM root volumes are cloned from a template copy kept on the pool's FlexVolume instead of being copied from secondary storage for every deployment. - Advertise CAN_CREATE_VOLUME_FROM_VOLUME from the datastore driver. - Create the template cache on primary: an iSCSI LUN (identity tracked on template_spool_ref) or a qcow2 file for NFS, populated by the KVM host. - Clone new volumes from the cached template: LUN clone for iSCSI and ONTAP file clone (FileCloneRequest) for NFS, growing the clone when the disk offering is larger than the template. - Clean up the cached LUN/file when the template is removed from the pool; a missing file is treated as already deleted. - Move protocol-specific create/clone/resize logic into UnifiedSANStrategy and UnifiedNASStrategy, and add a StorageStrategy helper to poll an optional async job. - Add unit tests for the driver and the NAS/SAN strategies.
1 parent 0a5bf30 commit 9ab7ec5

14 files changed

Lines changed: 2534 additions & 134 deletions

File tree

‎plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java‎

Lines changed: 541 additions & 48 deletions
Large diffs are not rendered by default.

‎plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
import feign.QueryMap;
2323
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
24+
import org.apache.cloudstack.storage.feign.model.FileCloneRequest;
2425
import org.apache.cloudstack.storage.feign.model.FileInfo;
26+
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
2527
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
2628
import feign.Headers;
2729
import feign.Param;
@@ -58,6 +60,15 @@ void createFile(@Param("authHeader") String authHeader,
5860
@Param("path") String filePath,
5961
FileInfo file);
6062

63+
/**
64+
* Creates a space-efficient clone of a file within a FlexVolume.
65+
*
66+
* <p>ONTAP REST: {@code POST /api/storage/file/clone}</p>
67+
*/
68+
@RequestLine("POST /api/storage/file/clone")
69+
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
70+
JobResponse cloneFile(@Param("authHeader") String authHeader, FileCloneRequest request);
71+
6172
// Export Policy Operations
6273
@RequestLine("POST /api/protocols/nfs/export-policies")
6374
@Headers({"Authorization: {authHeader}"})

‎plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public interface SANFeignClient {
5050
@Headers({"Authorization: {authHeader}"})
5151
Lun getLunByUUID(@Param("authHeader") String authHeader, @Param("uuid") String uuid);
5252

53-
@RequestLine("PATCH /{uuid}")
54-
@Headers({"Authorization: {authHeader}"})
53+
@RequestLine("PATCH /api/storage/luns/{uuid}")
54+
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
5555
void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun);
5656

5757
@RequestLine("DELETE /api/storage/luns/{uuid}")
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.cloudstack.storage.feign.model;
20+
21+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
22+
import com.fasterxml.jackson.annotation.JsonInclude;
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
25+
/**
26+
* Request body for the ONTAP file clone API.
27+
*
28+
* <p>ONTAP REST endpoint: {@code POST /api/storage/file/clone}</p>
29+
*
30+
* <p>Creates a space-efficient copy of a file. Source and destination paths are relative to the
31+
* root of {@code volume}, and both must live in that same FlexVolume.</p>
32+
*/
33+
@JsonIgnoreProperties(ignoreUnknown = true)
34+
@JsonInclude(JsonInclude.Include.NON_NULL)
35+
public class FileCloneRequest {
36+
37+
@JsonProperty("volume")
38+
private VolumeRef volume;
39+
40+
@JsonProperty("source_path")
41+
private String sourcePath;
42+
43+
@JsonProperty("destination_path")
44+
private String destinationPath;
45+
46+
@JsonProperty("overwrite_destination")
47+
private Boolean overwriteDestination;
48+
49+
public FileCloneRequest() {
50+
}
51+
52+
public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePath, String destinationPath) {
53+
this.volume = new VolumeRef(flexVolUuid, flexVolName);
54+
this.sourcePath = sourcePath;
55+
this.destinationPath = destinationPath;
56+
}
57+
58+
public VolumeRef getVolume() {
59+
return volume;
60+
}
61+
62+
public void setVolume(VolumeRef volume) {
63+
this.volume = volume;
64+
}
65+
66+
public String getSourcePath() {
67+
return sourcePath;
68+
}
69+
70+
public void setSourcePath(String sourcePath) {
71+
this.sourcePath = sourcePath;
72+
}
73+
74+
public String getDestinationPath() {
75+
return destinationPath;
76+
}
77+
78+
public void setDestinationPath(String destinationPath) {
79+
this.destinationPath = destinationPath;
80+
}
81+
82+
public Boolean getOverwriteDestination() {
83+
return overwriteDestination;
84+
}
85+
86+
public void setOverwriteDestination(Boolean overwriteDestination) {
87+
this.overwriteDestination = overwriteDestination;
88+
}
89+
90+
@JsonIgnoreProperties(ignoreUnknown = true)
91+
@JsonInclude(JsonInclude.Include.NON_NULL)
92+
public static class VolumeRef {
93+
94+
@JsonProperty("uuid")
95+
private String uuid;
96+
97+
@JsonProperty("name")
98+
private String name;
99+
100+
public VolumeRef() {
101+
}
102+
103+
public VolumeRef(String uuid, String name) {
104+
this.uuid = uuid;
105+
this.name = name;
106+
}
107+
108+
public String getUuid() {
109+
return uuid;
110+
}
111+
112+
public void setUuid(String uuid) {
113+
this.uuid = uuid;
114+
}
115+
116+
public String getName() {
117+
return name;
118+
}
119+
120+
public void setName(String name) {
121+
this.name = name;
122+
}
123+
}
124+
125+
@Override
126+
public String toString() {
127+
return "FileCloneRequest{volume=" + (volume != null ? volume.getUuid() : null)
128+
+ ", sourcePath=" + sourcePath
129+
+ ", destinationPath=" + destinationPath + "}";
130+
}
131+
}

‎plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ private String toIndentedString(Object o) {
308308
}
309309

310310

311+
@JsonInclude(JsonInclude.Include.NON_NULL)
311312
public static class Clone {
312313
@JsonProperty("source")
313314
private Source source = null;
@@ -319,6 +320,7 @@ public void setSource(Source source) {
319320
}
320321
}
321322

323+
@JsonInclude(JsonInclude.Include.NON_NULL)
322324
public static class Source {
323325
@JsonProperty("name")
324326
private String name = null;

‎plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java‎

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.cloudstack.storage.feign.model.Volume;
4141
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
4242
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
43+
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
4344
import org.apache.cloudstack.storage.service.model.AccessGroup;
4445
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
4546
import org.apache.cloudstack.storage.service.model.ProtocolType;
@@ -54,6 +55,8 @@
5455
import java.util.Map;
5556
import java.util.Objects;
5657

58+
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
59+
5760
/**
5861
* Storage Strategy represents the communication path for all the ONTAP storage options
5962
*
@@ -495,6 +498,27 @@ public String getNetworkInterface() {
495498
*/
496499
abstract public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume);
497500

501+
/**
502+
* Creates the protocol-specific backend object that caches a template on this pool.
503+
*
504+
* <p>iSCSI creates an empty LUN ({@code /vol/&lt;flexVol&gt;/cs_tmpl_&lt;id&gt;}) sized to
505+
* {@code sizeInBytes}. NFS is a no-op on the array: the KVM agent later writes the qcow2
506+
* into the mounted FlexVolume.</p>
507+
*
508+
* <p>Returns a {@link CloudStackVolume} so the driver can map it to {@code CreateCmdResult}
509+
* and update {@code template_spool_ref}. SAN populates {@code lun}; NAS returns an empty
510+
* volume (no LUN / file yet).</p>
511+
*
512+
* @param storagePool CloudStack primary storage pool (one FlexVolume)
513+
* @param templateInfo template being cached
514+
* @param details pool details (SVM, protocol, etc.)
515+
* @param sizeInBytes virtual size for the cache object (required for SAN; ignored for NAS)
516+
* @return created cache identity, or an empty {@link CloudStackVolume} when nothing is
517+
* pre-created on the array
518+
*/
519+
abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
520+
Map<String, String> details, long sizeInBytes);
521+
498522
/**
499523
* Method encapsulates the behavior based on the opted protocol in subclasses.
500524
* it is going to mimic
@@ -519,14 +543,28 @@ public String getNetworkInterface() {
519543
abstract public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume);
520544

521545
/**
522-
* Method encapsulates the behavior based on the opted protocol in subclasses.
546+
* Creates a space-efficient clone of an existing object inside the same FlexVolume.
523547
* it is going to mimic
524548
* cloneLun for iSCSI, FC protocols
525549
* cloneFile for NFS3.0 and NFS4.1 protocols
526550
* cloneNameSpace for Nvme/TCP and Nvme/FC protocol
527-
* @param cloudstackVolume the CloudStack volume to copy
551+
*
552+
* <p>ONTAP requires the source and the destination to live in the same FlexVolume, which
553+
* holds because a CloudStack primary storage pool maps one-to-one onto a FlexVolume.</p>
554+
*
555+
* @param cloudstackVolume describes the clone to create; the source is carried in the
556+
* protocol-specific clone reference (for SAN, {@code lun.clone.source})
557+
* @return the created CloudStackVolume, populated with the backend identity of the clone
528558
*/
529-
abstract public void copyCloudStackVolume(CloudStackVolume cloudstackVolume);
559+
abstract public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume);
560+
561+
/**
562+
* Grows an existing backend object to {@code sizeInBytes}.
563+
*
564+
* <p>Needed after cloning a cached template, because a clone inherits the size of its source
565+
* while the service offering may ask for a larger disk.</p>
566+
*/
567+
abstract public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes);
530568

531569
/**
532570
* Method encapsulates the behavior based on the opted protocol in subclasses.
@@ -694,4 +732,22 @@ public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeIn
694732
}
695733
return true;
696734
}
735+
736+
/**
737+
* Polls an ONTAP async job when the API response includes a job reference.
738+
*
739+
* <p>When no job is returned (synchronous completion), the operation is treated as
740+
* successful after HTTP 2xx.</p>
741+
*
742+
* @param response ONTAP job response (may be null or without a job)
743+
* @param operationName label for logging and error messages
744+
*/
745+
public void pollJobIfPresent(JobResponse response, String operationName) {
746+
if (response == null || response.getJob() == null || response.getJob().getUuid() == null) {
747+
logger.debug("pollJobIfPresent: No async job returned for operation [{}], continuing without polling",
748+
operationName);
749+
return;
750+
}
751+
jobPollForSuccess(response.getJob().getUuid(), 60, 2000);
752+
}
697753
}

0 commit comments

Comments
 (0)