diff --git a/core/src/main/java/com/cloud/resource/ServerResourceBase.java b/core/src/main/java/com/cloud/resource/ServerResourceBase.java index 28f15e63ee9e..4f26ed59d649 100644 --- a/core/src/main/java/com/cloud/resource/ServerResourceBase.java +++ b/core/src/main/java/com/cloud/resource/ServerResourceBase.java @@ -293,10 +293,6 @@ public Answer listHostLunDevices(Command command) { } try { - if (ListHostLunDeviceCommand.MODE_SINGLE.equals(lunPathMode)) { - return new ListHostLunDeviceAnswer(true, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); - } - ListHostLunDeviceAnswer fast = listHostLunDevicesFast(lunPathMode); if (fast != null && fast.getResult()) { return fast; @@ -1959,11 +1955,11 @@ private boolean hasPartitionRecursive(JSONObject device) { return false; } - if ("lvm".equals(deviceType) || deviceName.startsWith("/dev/mapper/")) { - if (deviceName.contains("ceph--") && deviceName.contains("--osd--block--")) { - return false; - } - return true; + + if (deviceName.startsWith("/dev/mapper/") + && deviceName.contains("ceph--") + && deviceName.contains("--osd--block--")) { + return false; } if (device.has("children")) { @@ -1972,16 +1968,11 @@ private boolean hasPartitionRecursive(JSONObject device) { for (int i = 0; i < children.length(); i++) { JSONObject child = children.getJSONObject(i); String childType = child.optString("type", ""); - String childName = child.optString("name", ""); if ("part".equals(childType)) { return true; } - if ("lvm".equals(childType)) { - return true; - } - if (hasPartitionRecursive(child)) { return true; } @@ -4082,15 +4073,17 @@ private String validateLunDeviceForAttachment(String devicePath, String vmName) return "파티션 디바이스는 할당할 수 없습니다. 전체 디스크를 사용해주세요: " + devicePath; } - if (isLunDeviceAllocatedToOtherVm(devicePath, vmName)) { - return "디바이스가 이미 다른 VM에 할당되어 있습니다: " + devicePath; + String allocatedVmName = findLunDeviceAllocatedVmName(devicePath, vmName); + if (allocatedVmName != null) { + return "디바이스가 이미 다른 VM에 할당되어 있습니다: " + devicePath + " (VM: " + allocatedVmName + ")"; } Map mappings = buildDeviceMapping(); DeviceMapping mapping = findMappedDevice(devicePath, mappings); if (mapping != null && mapping.getScsiDevicePath() != null) { - if (isLunDeviceAllocatedToOtherVm(mapping.getScsiDevicePath(), vmName)) { - return "매핑된 SCSI 디바이스가 이미 다른 VM에 할당되어 있습니다: " + mapping.getScsiDevicePath(); + String mappedAllocatedVmName = findLunDeviceAllocatedVmName(mapping.getScsiDevicePath(), vmName); + if (mappedAllocatedVmName != null) { + return "매핑된 SCSI 디바이스가 이미 다른 VM에 할당되어 있습니다: " + mapping.getScsiDevicePath() + " (VM: " + mappedAllocatedVmName + ")"; } } @@ -4185,29 +4178,41 @@ private boolean isPartitionDevice(String devicePath) { } private boolean isLunDeviceAllocatedToOtherVm(String devicePath, String currentVmName) { + return findLunDeviceAllocatedVmName(devicePath, currentVmName) != null; + } + + private String findLunDeviceAllocatedVmName(String devicePath, String currentVmName) { try { Script listCommand = new Script("/bin/bash"); listCommand.add("-c"); - listCommand.add("virsh list --all | grep -v 'Id' | grep -v '^-' | while read line; do " + - "vm_id=$(echo $line | awk '{print $1}'); " + - "if [ ! -z \"$vm_id\" ] && [ \"$vm_id\" != \"" + currentVmName + "\" ]; then " + - "virsh dumpxml $vm_id | grep -q '" + devicePath + "'; " + + listCommand.add("virsh list --all --name | while read vm_name; do " + + "if [ -z \"$vm_name\" ] || [ \"$vm_name\" = \"" + currentVmName + "\" ]; then " + + "continue; " + + "fi; " + + "virsh dumpxml \"$vm_name\" | grep -Fq \"dev='" + devicePath + "'\"; " + + "if [ $? -ne 0 ]; then " + + "virsh dumpxml \"$vm_name\" | grep -Fq 'dev=\"" + devicePath + "\"'; " + + "fi; " + "if [ $? -eq 0 ]; then " + "echo 'allocated'; " + + "echo \"$vm_name\"; " + "break; " + "fi; " + - "fi; " + "done"); OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); String result = listCommand.execute(parser); if (result == null && parser.getLines() != null) { - return parser.getLines().trim().equals("allocated"); + String[] lines = parser.getLines().trim().split("\\r?\\n"); + if (lines.length >= 2 && "allocated".equals(lines[0].trim())) { + String vmName = lines[1].trim(); + return vmName.isEmpty() ? "unknown" : vmName; + } } - return false; + return null; } catch (Exception e) { logger.debug("Error checking device allocation to other VMs: {}", e.getMessage()); - return false; + return null; } } diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue index 35b5f6db3d8a..3a50f73c7749 100644 --- a/ui/src/views/compute/InstanceTab.vue +++ b/ui/src/views/compute/InstanceTab.vue @@ -500,7 +500,7 @@ export default { formattedText = formattedText.replace(/(Revision:\s+[^\s\n]+)( +)(?=\S)/gi, '$1\n$2') - formattedText = formattedText.replace(/HAS_PARTITIONS:\s*false/gi, this.$t('label.no.partitions')) + formattedText = formattedText.replace(/HAS_PARTITIONS:\s*false/gi, '') formattedText = formattedText.replace(/HAS_PARTITIONS:\s*true/gi, this.$t('label.has.partitions')) formattedText = formattedText.replace(/USAGE_STATUS:\s*사용안함/gi, '사용안함') @@ -724,6 +724,21 @@ export default { } }) + const hostId = this.vm?.hostid || categorized.lun?.[0]?.hostId + if (hostId && categorized.lun.length > 0) { + const lunDetailMap = await this.fetchLunDetailMap(hostId) + categorized.lun = categorized.lun.map(device => { + if (device.hostDevicesText && String(device.hostDevicesText).trim().length > 0) { + return device + } + const detail = lunDetailMap[device.hostDevicesName] + if (detail) { + return { ...device, hostDevicesText: detail } + } + return device + }) + } + this.pciDevices = categorized.pci this.usbDevices = categorized.usb this.lunDevices = categorized.lun @@ -747,6 +762,44 @@ export default { return this.deviceAssignmentsPromise }, + async fetchLunDetailMap (hostId) { + const detailMap = {} + try { + const [singleSettled, multiSettled] = await Promise.allSettled([ + api('listHostLunDevices', { id: hostId, lunpathmode: 'single', lunPathMode: 'single' }), + api('listHostLunDevices', { id: hostId, lunpathmode: 'multipath', lunPathMode: 'multipath' }) + ]) + + const sources = [] + if (singleSettled.status === 'fulfilled') { + sources.push(singleSettled.value?.listhostlundevicesresponse?.listhostlundevices?.[0]) + } + if (multiSettled.status === 'fulfilled') { + sources.push(multiSettled.value?.listhostlundevicesresponse?.listhostlundevices?.[0]) + } + + sources.forEach(src => { + if (!src) return + if (src.devicedetails && typeof src.devicedetails === 'object') { + Object.entries(src.devicedetails).forEach(([name, text]) => { + if (name && text && !detailMap[name]) { + detailMap[name] = text + } + }) + } + const names = Array.isArray(src.hostdevicesname) ? src.hostdevicesname : [] + const texts = Array.isArray(src.hostdevicestext) ? src.hostdevicestext : [] + names.forEach((name, idx) => { + const text = texts[idx] + if (name && text && !detailMap[name]) { + detailMap[name] = text + } + }) + }) + } catch (e) { + } + return detailMap + }, async fetchPciDevices () { await this.loadDevicesFromDb() }, @@ -893,7 +946,6 @@ export default { } -