|
23 | 23 | import java.security.KeyManagementException; |
24 | 24 | import java.security.KeyStoreException; |
25 | 25 | import java.security.NoSuchAlgorithmException; |
26 | | -import java.text.SimpleDateFormat; |
| 26 | +import java.time.ZoneOffset; |
| 27 | +import java.time.format.DateTimeFormatter; |
27 | 28 | import java.util.ArrayList; |
28 | 29 | import java.util.HashMap; |
29 | 30 | import java.util.Map; |
@@ -88,6 +89,9 @@ public class FlashArrayAdapter implements ProviderAdapter { |
88 | 89 | static final ObjectMapper mapper = new ObjectMapper(); |
89 | 90 | public String pod = null; |
90 | 91 | public String hostgroup = null; |
| 92 | + private static final DateTimeFormatter DELETION_TIMESTAMP_FORMAT = |
| 93 | + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC); |
| 94 | + |
91 | 95 | private String username; |
92 | 96 | private String password; |
93 | 97 | private String accessToken; |
@@ -200,28 +204,63 @@ public void detach(ProviderAdapterContext context, ProviderAdapterDataObject dat |
200 | 204 |
|
201 | 205 | @Override |
202 | 206 | public void delete(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) { |
203 | | - // first make sure we are disconnected |
204 | | - removeVlunsAll(context, pod, dataObject.getExternalName()); |
205 | 207 | String fullName = normalizeName(pod, dataObject.getExternalName()); |
206 | 208 |
|
207 | | - FlashArrayVolume volume = new FlashArrayVolume(); |
| 209 | + // Snapshots live under /volume-snapshots and already use the array's |
| 210 | + // reserved form <volume>.<suffix>, which legitimately contains ".". |
| 211 | + // The stricter [A-Za-z0-9_-] naming rule applies to regular volume |
| 212 | + // names and free-form rename targets, not to these reserved snapshot |
| 213 | + // names. Since FlashArray snapshot names are system-defined rather |
| 214 | + // than arbitrary rename targets, we skip the usual timestamped rename |
| 215 | + // and only mark snapshots destroyed; the array's own ".N" suffix |
| 216 | + // already disambiguates them in the recycle bin. |
| 217 | + if (dataObject.getType() == ProviderAdapterDataObject.Type.SNAPSHOT) { |
| 218 | + try { |
| 219 | + FlashArrayVolume destroy = new FlashArrayVolume(); |
| 220 | + destroy.setDestroyed(true); |
| 221 | + PATCH("/volume-snapshots?names=" + fullName, destroy, new TypeReference<FlashArrayList<FlashArrayVolume>>() { |
| 222 | + }); |
| 223 | + } catch (CloudRuntimeException e) { |
| 224 | + String msg = e.getMessage(); |
| 225 | + if (msg != null && (msg.contains("No such volume or snapshot") |
| 226 | + || msg.contains("Volume does not exist"))) { |
| 227 | + return; |
| 228 | + } |
| 229 | + throw e; |
| 230 | + } |
| 231 | + return; |
| 232 | + } |
208 | 233 |
|
209 | | - // rename as we delete so it doesn't conflict if the template or volume is ever recreated |
210 | | - // pure keeps the volume(s) around in a Destroyed bucket for a period of time post delete |
211 | | - String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date()); |
212 | | - volume.setExternalName(fullName + "-" + timestamp); |
| 234 | + // first make sure we are disconnected |
| 235 | + removeVlunsAll(context, pod, dataObject.getExternalName()); |
| 236 | + |
| 237 | + // Rename then destroy: FlashArray keeps destroyed volumes in a recycle |
| 238 | + // bin (default 24h) from which they can be recovered. Renaming with a |
| 239 | + // deletion timestamp gives operators a forensic trail when browsing the |
| 240 | + // array - they can see when each destroyed copy was deleted on the |
| 241 | + // CloudStack side. FlashArray rejects a single PATCH that combines |
| 242 | + // {name, destroyed}, so the rename and the destroy must be issued as |
| 243 | + // two separate requests each carrying only its own field. |
| 244 | + // Use UTC so the rename suffix is stable regardless of the management |
| 245 | + // server's local timezone or DST changes - operators correlating the |
| 246 | + // CloudStack delete event with the array's audit log get a consistent |
| 247 | + // wall-clock value. |
| 248 | + String timestamp = DELETION_TIMESTAMP_FORMAT.format(java.time.Instant.now()); |
| 249 | + String renamedName = fullName + "-" + timestamp; |
213 | 250 |
|
214 | 251 | try { |
215 | | - PATCH("/volumes?names=" + fullName, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() { |
| 252 | + FlashArrayVolume rename = new FlashArrayVolume(); |
| 253 | + rename.setExternalName(renamedName); |
| 254 | + PATCH("/volumes?names=" + fullName, rename, new TypeReference<FlashArrayList<FlashArrayVolume>>() { |
216 | 255 | }); |
217 | 256 |
|
218 | | - // now delete it with new name |
219 | | - volume.setDestroyed(true); |
220 | | - |
221 | | - PATCH("/volumes?names=" + fullName + "-" + timestamp, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() { |
| 257 | + FlashArrayVolume destroy = new FlashArrayVolume(); |
| 258 | + destroy.setDestroyed(true); |
| 259 | + PATCH("/volumes?names=" + renamedName, destroy, new TypeReference<FlashArrayList<FlashArrayVolume>>() { |
222 | 260 | }); |
223 | 261 | } catch (CloudRuntimeException e) { |
224 | | - if (e.toString().contains("Volume does not exist")) { |
| 262 | + String msg = e.getMessage(); |
| 263 | + if (msg != null && msg.contains("Volume does not exist")) { |
225 | 264 | return; |
226 | 265 | } else { |
227 | 266 | throw e; |
|
0 commit comments