Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -88,6 +89,9 @@ public class FlashArrayAdapter implements ProviderAdapter {
static final ObjectMapper mapper = new ObjectMapper();
public String pod = null;
public String hostgroup = null;
private static final DateTimeFormatter DELETION_TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC);

private String username;
private String password;
private String accessToken;
Expand Down Expand Up @@ -200,28 +204,63 @@ public void detach(ProviderAdapterContext context, ProviderAdapterDataObject dat

@Override
public void delete(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
// first make sure we are disconnected
removeVlunsAll(context, pod, dataObject.getExternalName());
String fullName = normalizeName(pod, dataObject.getExternalName());

FlashArrayVolume volume = new FlashArrayVolume();
// Snapshots live under /volume-snapshots and already use the
// reserved form <volume>.<suffix>. FlashArray volume/snapshot names
// must match [A-Za-z0-9_-] and start/end with an alphanumeric, so
// appending our usual deletion-timestamp suffix to a snapshot name
// would produce a target like "<vol>.<n>-<ts>" - the embedded "."
// is rejected by the array. We therefore skip the rename for
// snapshots and only mark them destroyed; the array's own ".N"
// suffix already disambiguates them in the recycle bin.
if (dataObject.getType() == ProviderAdapterDataObject.Type.SNAPSHOT) {
try {
FlashArrayVolume destroy = new FlashArrayVolume();
destroy.setDestroyed(true);
PATCH("/volume-snapshots?names=" + fullName, destroy, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
Comment on lines +221 to +222
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names= query parameter is built via string concatenation without URL-encoding. If fullName/renamedName contains reserved characters (e.g., : seen in cloudstack::..., or any future name changes), the request can be mis-parsed by the HTTP stack or server. Build the URI with proper query encoding (e.g., encode the names value or use a URI/query builder) before calling PATCH.

Copilot uses AI. Check for mistakes.
} catch (CloudRuntimeException e) {
String msg = e.getMessage();
if (msg != null && (msg.contains("No such volume or snapshot")
|| msg.contains("Volume does not exist"))) {
return;
}
throw e;
}
Comment on lines +223 to +230
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching error conditions via e.toString().contains(...) is brittle (it can include class names, nested exceptions, and formatting changes). Prefer checking a stable signal such as e.getMessage(), an HTTP status code/response body field from the underlying API client (if accessible), or a dedicated helper that inspects the FlashArray error payload. This will reduce false positives/negatives and improve long-term reliability.

Copilot uses AI. Check for mistakes.
return;
}

// rename as we delete so it doesn't conflict if the template or volume is ever recreated
// pure keeps the volume(s) around in a Destroyed bucket for a period of time post delete
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date());
volume.setExternalName(fullName + "-" + timestamp);
// first make sure we are disconnected
removeVlunsAll(context, pod, dataObject.getExternalName());

// Rename then destroy: FlashArray keeps destroyed volumes in a recycle
// bin (default 24h) from which they can be recovered. Renaming with a
// deletion timestamp gives operators a forensic trail when browsing the
// array - they can see when each destroyed copy was deleted on the
// CloudStack side. FlashArray rejects a single PATCH that combines
// {name, destroyed}, so the rename and the destroy must be issued as
// two separate requests each carrying only its own field.
// Use UTC so the rename suffix is stable regardless of the management
// server's local timezone or DST changes - operators correlating the
// CloudStack delete event with the array's audit log get a consistent
// wall-clock value.
String timestamp = DELETION_TIMESTAMP_FORMAT.format(java.time.Instant.now());
String renamedName = fullName + "-" + timestamp;

try {
PATCH("/volumes?names=" + fullName, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
FlashArrayVolume rename = new FlashArrayVolume();
rename.setExternalName(renamedName);
PATCH("/volumes?names=" + fullName, rename, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
Comment on lines +254 to 255
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names= query parameter is built via string concatenation without URL-encoding. If fullName/renamedName contains reserved characters (e.g., : seen in cloudstack::..., or any future name changes), the request can be mis-parsed by the HTTP stack or server. Build the URI with proper query encoding (e.g., encode the names value or use a URI/query builder) before calling PATCH.

Copilot uses AI. Check for mistakes.

// now delete it with new name
volume.setDestroyed(true);

PATCH("/volumes?names=" + fullName + "-" + timestamp, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
FlashArrayVolume destroy = new FlashArrayVolume();
destroy.setDestroyed(true);
PATCH("/volumes?names=" + renamedName, destroy, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
Comment on lines +259 to 260
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names= query parameter is built via string concatenation without URL-encoding. If fullName/renamedName contains reserved characters (e.g., : seen in cloudstack::..., or any future name changes), the request can be mis-parsed by the HTTP stack or server. Build the URI with proper query encoding (e.g., encode the names value or use a URI/query builder) before calling PATCH.

Copilot uses AI. Check for mistakes.
} catch (CloudRuntimeException e) {
if (e.toString().contains("Volume does not exist")) {
String msg = e.getMessage();
if (msg != null && msg.contains("Volume does not exist")) {
return;
} else {
throw e;
Expand Down
Loading