Copilot commented on code in PR #14203:
URL: https://github.com/apache/cloudstack/pull/14203#discussion_r4399329448


##########
plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java:
##########
@@ -455,7 +475,57 @@ public boolean createFolder(String uuid, String path, 
String localPath) {
     @Override
     public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool 
pool, PhysicalDiskFormat format,
             ProvisioningType provisioningType, long size, byte[] passphrase) {
-        return null;
+        SP_LOG("StorPoolStorageAdaptor.createPhysicalDisk: name=%s, pool=%s, 
size=%d", name, pool.getUuid(), size);
+
+        if (passphrase != null) {
+            throw new CloudRuntimeException("Encrypted StorPool volumes are 
not supported");
+        }
+
+        if (!PhysicalDiskFormat.RAW.equals(format)) {
+            throw new CloudRuntimeException(String.format("StorPool volumes 
can only be created as RAW, %s was requested", format));
+        }
+
+        if (size <= 0) {
+            throw new CloudRuntimeException("Size must be greater than 0 to 
create a StorPool volume");
+        }
+
+        String[] uuidParts = pool.getUuid() != null ? 
pool.getUuid().split(";") : new String[0];
+        if (uuidParts.length == 0 || StringUtils.isBlank(uuidParts[0])) {
+            throw new CloudRuntimeException("Unable to resolve the StorPool 
template for pool " + pool.getUuid());
+        }
+        String template = uuidParts[0];
+
+        Map<String, String> tags = new HashMap<>();
+        tags.put("cs", "volume");
+        tags.put("uuid", name);
+
+        OutputInterpreter.AllLinesParser parser = 
createStorPoolVolume(template, size, tags,
+                String.format(" for %s", name));
+
+        String globalId = getNameFromResponse(parser.getLines(), false, false);
+        if (StringUtils.isBlank(globalId)) {
+            LOGGER.warn(String.format("StorPool volume for %s was created but 
its globalId could not be parsed from the response; " +
+                    "it must be found (tag uuid=%s) and deleted manually to 
avoid an orphaned volume", name, name));
+            throw new CloudRuntimeException(String.format("StorPool did not 
return a volume name/globalId when creating %s", name));
+        }
+
+        String volumePath = StorPoolUtil.devPath(globalId);
+        try {
+            if (!attachOrDetachVolume("attach", "volume", volumePath)) {
+                // The attach succeeded but the device never showed up; 
StorPool refuses to delete an attached volume.
+                attachOrDetachVolume("detach", "volume", volumePath);
+                throw new CloudRuntimeException(String.format("StorPool volume 
%s for %s was attached but its device did not appear", volumePath, name));
+            }
+        } catch (CloudRuntimeException e) {
+            volumeDelete("~" + globalId);
+            throw e;
+        }

Review Comment:
   If `volumeDelete(...)` fails in this catch block, its exception replaces the 
original error `e`, and no log line says the created volume still exists. Wrap 
the cleanup in its own try, log a manual-delete warning on failure, then 
rethrow `e`.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -757,10 +924,13 @@ backup_stopped_vm() {
   mount_operation
   mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 
1; }
 
-  IFS=","
+  local -a disk_arr=()
+  IFS=',' read -ra disk_arr <<< "$DISK_PATHS"

Review Comment:
   If `DISK_PATHS` is empty, the array is empty, the loop never runs, and the 
script still prints `BACKUP_SIZE_TOTAL=0` and exits 0. This happens when 
libvirt reports the VM as shut off but CloudStack sent no disk paths. Before 
this change, the empty output made the Java size parse fail; now an empty 
backup is recorded as a success. Fail the stopped-VM path when there are no 
disks to read.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -406,21 +410,184 @@ backup_running_vm_pull() {
   # End the pull job. Aborting the active job is how a pull backup finishes 
(there is no
   # backup-end command): libvirt tears down the NBD server and releases the 
scratch devices,
   # which the agent then deletes. The data already pulled is a complete 
backup, so from here
-  # on a failure must no longer discard it.
-  trap - EXIT INT TERM HUP
+  # on a failure must no longer discard it. Hand EXIT back to the StorPool 
safety net.
+  trap - INT TERM HUP
+  trap sp_cleanup_backup_source_disks EXIT
   if ! abort_backup_job; then
     log -e "warning: failed to end (domjobabort) backup job for $VM"
   fi
   rm -rf "$work_dir"
   sync
 
-  # Statistics -- the size on the last stdout line is parsed by the Java 
wrapper.
-  du -sb $dest | cut -f1
+  backup_size=$(du -sb "$dest" 2>>"$logFile" | cut -f1) || { log -ne "WARNING: 
du failed for $dest, reporting size as 0"; backup_size=0; }
+  timeout "$UNMOUNT_TIMEOUT" umount "$mount_point" 2>>"$logFile" || { log 
"WARNING: umount of $mount_point failed or timed out"; true; }
+  rmdir "$mount_point" 2>>"$logFile" || { log "WARNING: rmdir of $mount_point 
failed"; true; }
+  echo "BACKUP_SIZE_TOTAL=$backup_size"
+}
 
-  umount $mount_point
-  rmdir $mount_point
+# StorPool volume name (with the "~globalId" form the storpool/storpool_req 
CLIs expect) from
+# /dev/storpool-byid/<globalId>.
+sp_volume_name_from_path() {
+  local fullpath="$1"
+  local name
+  if [[ "$fullpath" == /dev/storpool-byid/* ]]; then
+    name="${fullpath#/dev/storpool-byid/}"
+    name="~${name%%/*}"
+  else
+    return 1
+  fi
+  echo "$name"
+}
+
+# Poll for the device node to actually appear after a successful attach: udev 
creates the
+# symlink asynchronously, so a successful "attach" CLI call doesn't guarantee 
the device path
+# is usable yet.
+sp_wait_for_device_symlink() {
+  local name="$1"
+  local devpath="/dev/storpool-byid/${name#\~}"
+  local tries=10 i
+  for ((i = 0; i < tries; i++)); do
+    if [[ -e "$devpath" ]] && [[ "$(blockdev --getsize64 "$devpath" 
2>/dev/null || echo 0)" -gt 0 ]]; then
+      return 0
+    fi
+    sleep 0.1
+  done
+  return 1
+}
+
+# A successful attach also waits for the device symlink.
+sp_attach_volume() {
+  local name="$1"
+  storpool -M -B attach volume "$name" here onRemoteAttached export 
>>"$logFile" 2>&1 && { sp_wait_for_device_symlink "$name"; return $?; }
+  return 1
+}
+
+sp_api_post() {
+  local query="$1" json="$2"
+  local key value host="" port="81" token=""
+  while IFS='=' read -r key value; do
+    case "$key" in
+      SP_API_HTTP_HOST) host="$value" ;;
+      SP_API_HTTP_PORT) port="$value" ;;
+      SP_AUTH_TOKEN) token="$value" ;;
+    esac
+  done < <(storpool_confget 2>>"$logFile")
+  if [[ -z "$host" || -z "$token" ]]; then
+    log -ne "StorPool: SP_API_HTTP_HOST or SP_AUTH_TOKEN missing from 
storpool_confget, cannot call $query"
+    return 1
+  fi
+
+  local resp
+  if ! resp=$(curl -sS --connect-timeout 10 -X POST \
+      -H @<(printf 'Authorization: Storpool v1:%s\n' "$token") \
+      -H "Content-Type: application/json" -d "$json" \
+      "http://$host:$port/ctrl/1.0/$query"; 2>>"$logFile"); then
+    log -ne "StorPool: API call $query failed, see above for curl output"
+    return 1
+  fi
+  python3 -c '
+import sys, json
+try:
+    resp = json.load(sys.stdin)
+except Exception:
+    sys.exit(1)
+if "error" in resp or "data" not in resp:
+    sys.exit(1)
+print(json.dumps(resp["data"]))
+' <<< "$resp" || {
+    log -ne "StorPool: API call $query returned an error: $resp"
+    return 1
+  }
+}
+
+# Deletes $1, force-detaching it first wherever it is still attached (StorPool 
refuses to delete
+# an attached volume). Returns 1, after logging, if the delete failed.
+sp_delete_volume() {
+  local volume_name="$1"
+  if ! sp_api_post "MultiCluster/VolumeDelete/$volume_name" 
'{"onAttached":"detachForce"}' >/dev/null; then
+    log -ne "StorPool: failed to delete volume $volume_name, it must be 
deleted manually"
+    return 1
+  fi
 }
 
+# Clones $1 (a live StorPool volume path) into a new, point-in-time StorPool 
volume via the
+# VolumeCreate API's baseOn option, attaches it, and echoes the clone's device 
path. Only called
+# once this script's own (fresh, right-here) VM liveness check has already 
routed to the cold
+# path — see backup_stopped_vm.
+sp_create_backup_source_disk() {
+  local volume_path="$1"
+  local base_on
+  if ! base_on=$(sp_volume_name_from_path "$volume_path"); then
+    echo "Could not resolve a StorPool volume name from path $volume_path to 
create a backup source volume" >&2
+    return 1
+  fi
+
+  log -ne "StorPool: creating backup source volume based on $base_on (from 
$volume_path)"
+
+  local resp
+  if ! resp=$(sp_api_post "MultiCluster/VolumeCreate" 
"{\"baseOn\":\"$base_on\",\"tags\":{\"cs\":\"backup\"}}"); then
+    log -ne "StorPool: VolumeCreate baseOn=$base_on failed, see above"
+    echo "Could not create a backup source volume based on $base_on" >&2
+    return 1
+  fi
+
+  local global_id
+  global_id=$(python3 -c '
+import sys, json
+try:
+    data = json.load(sys.stdin)
+    gid = data.get("globalId")
+except Exception:
+    gid = None
+if not gid:
+    sys.exit(1)
+print(gid)
+' <<< "$resp" 2>>"$logFile") || {
+    log -ne "StorPool: VolumeCreate baseOn=$base_on returned no globalId, 
response: $resp -- the volume was created but its name could not be parsed; it 
must be found (tag cs=backup, baseOn=$base_on) and deleted manually to avoid an 
orphaned volume"
+    echo "StorPool did not return a volume name when cloning $base_on for 
backup" >&2
+    return 1
+  }
+
+  local clone_path="/dev/storpool-byid/$global_id"
+  local clone_name="~$global_id"
+  log -ne "StorPool: created backup source volume $clone_name ($clone_path), 
attaching"
+  if ! sp_attach_volume "$clone_name"; then
+    log -ne "StorPool: failed to attach backup source volume $clone_name, 
deleting it"
+    sp_delete_volume "$clone_name" || true
+    echo "Could not attach backup source volume $clone_path cloned from 
$base_on" >&2
+    return 1
+  fi
+
+  # Not registered in SP_CLEANUP_VOLUMES here: the caller always invokes this 
function via
+  # command substitution ($(...)), which runs it in a subshell — any append to 
the array here
+  # would be a mutation of that subshell's own copy and vanish once it exits. 
The caller must
+  # register $clone_name itself right after reading it back from stdout below.
+  log -ne "StorPool: backup source volume $clone_name attached at $clone_path, 
reading from it for this backup"
+  echo "$clone_path"
+}
+
+# Deletes a volume previously returned by sp_create_backup_source_disk. 
Best-effort:
+# logs on failure rather than aborting the backup, matching the wrapper's old 
cleanup semantics.
+sp_delete_backup_source_disk() {
+  local clone_name="$1"
+  log -ne "StorPool: releasing backup source volume $clone_name"
+  sp_delete_volume "$clone_name"
+}
+
+# Safety net for any clone not already released by its own per-disk cleanup 
(e.g. the script
+# exiting between creating it and reaching that point). A cleanup here means 
an earlier step
+# didn't run its normal release — worth noticing, so it's logged even though 
it isn't fatal.
+sp_cleanup_backup_source_disks() {
+  local clone_name
+  for clone_name in "${SP_CLEANUP_VOLUMES[@]:-}"; do
+    [[ -z "$clone_name" ]] && continue
+    log -ne "StorPool: cleaning up backup source volume $clone_name left over 
on script exit"
+    sp_delete_backup_source_disk "$clone_name"

Review Comment:
   If `sp_delete_backup_source_disk` fails, its non-zero status makes `set -e` 
(line 20) exit the shell inside this EXIT trap. The remaining clones in 
`SP_CLEANUP_VOLUMES` are never attempted, and on the pull path `cleanup` is 
skipped. Make the call best-effort, as the create-failure path in 
`sp_create_backup_source_disk` already does with `|| true`.



##########
test/integration/plugins/storpool/TestNasBackupStorPool.py:
##########
@@ -0,0 +1,716 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Smoke tests for NAS backup and recovery on StorPool primary storage.
+
+Assumes the environment already has NAS backup configured for the zone
+(a NAS backup repository and an imported backup offering).
+"""
+
+import math
+import time
+import uuid
+
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.codes import FAILED, KVM
+from marvin.lib.base import (Account,
+                             Backup,
+                             BackupOffering,
+                             BackupRepository,
+                             DiskOffering,
+                             SecurityGroup,
+                             ServiceOffering,
+                             StoragePool,
+                             VirtualMachine,
+                             Volume)

Review Comment:
   `Account` and `BackupRepository` are imported in this module but never used. 
Remove them from the import list to avoid lint failures.



##########
test/integration/plugins/storpool/sp_util.py:
##########
@@ -21,6 +21,7 @@
 from marvin.lib.base import (Account,
                              Cluster,
                              Configurations,
+                             ImageStore,
                              ServiceOffering,

Review Comment:
   The `ImageStore` import is added but never used in sp_util.py. Only the 
import line references it, so this will be reported as an unused import by the 
Python lint checks.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to