Skip to content

Commit 7085da1

Browse files
committed
Fix V2K and N2K disk ordering bugs
1 parent 00bf288 commit 7085da1

9 files changed

Lines changed: 425 additions & 4 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# V2K/N2K 디스크 순서 보정 설계
2+
3+
## 배경
4+
5+
N2K wizard 테스트에서 Nutanix inventory 배열 첫 번째 디스크가 실제 `SCSI unit 0`이 아닌 `SCSI unit 1`로 내려왔고, n2k가 배열 첫 번째 디스크를 Cloud ROOT 볼륨으로 사용하면서 대상 VM의 root/data 디스크 역할이 뒤바뀌었다.
6+
7+
V2K 코드도 Cloud cutover, libvirt XML 생성, Linux bootstrap 단계에서 `.disks[0]`을 root 디스크로 간주한다. VMware inventory는 `disk_id``scsi0:0`처럼 주소 기반으로 만들지만, govc device 배열을 명시적으로 정렬하지 않으면 같은 계열의 오류가 발생할 수 있다.
8+
9+
## 원칙
10+
11+
- 외부 API가 반환한 디스크 배열 순서를 root 판정 근거로 사용하지 않는다.
12+
- 명시적인 boot disk 정보가 있으면 가장 우선한다.
13+
- boot disk 정보가 없으면 컨트롤러 타입, 버스, 유닛 번호 기준으로 안정 정렬한다.
14+
- 정렬 후 첫 번째 디스크는 `role=root`, 나머지는 `role=data`로 기록해 후속 분석과 로그 판독을 쉽게 한다.
15+
- wizard target map, manifest, phase1/phase2/full cutover가 동일한 정렬 결과를 공유하도록 inventory 정규화 단계에서 보정한다.
16+
17+
## N2K 적용
18+
19+
- Nutanix inventory 정규화 단계에서 `bootConfig.bootDevice.diskAddress`가 있으면 해당 디스크를 root로 우선 정렬한다.
20+
- boot 주소가 없으면 `controller.type`, `controller.bus`, `controller.unit/device_index`, 원본 배열 인덱스 순으로 정렬한다.
21+
- 정렬 후 fallback label과 `role`을 부여한다.
22+
23+
## V2K 적용
24+
25+
- VMware inventory 정규화 단계에서 `config.bootOptions.bootOrder[].deviceKey`가 있으면 해당 `VirtualDisk.key`를 가진 디스크를 root로 우선 정렬한다.
26+
- boot order가 없거나 디스크 항목이 없으면 `controller.type`, `controller.bus`, `controller.unit`, 원본 배열 인덱스 순으로 정렬한다.
27+
- 정렬 후 `role=root|data``source_ordinal`을 기록한다.
28+
29+
## 검증
30+
31+
- N2K: Nutanix API 배열이 `unit 1`, `unit 0` 순서로 내려와도 `unit 0``.disks[0]`이 되는 smoke test를 추가한다.
32+
- V2K: govc `device.info` 배열이 `scsi0:1`, `scsi0:0` 순서로 내려와도 `scsi0:0``.disks[0]`이 되는 smoke test를 추가한다.
33+
- V2K: boot order가 `scsi0:1``deviceKey`를 명시하면 `scsi0:1`을 root로 우선하는 smoke test를 추가한다.

lib/n2k/nutanix_api.sh

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,39 @@ n2k_nutanix_inventory_from_raw() {
399399
and (((.device_properties.device_type // .deviceProperties.deviceType // .device_type // .deviceType // "DISK") | tostring | ascii_upcase) != "CDROM")
400400
));
401401
402+
def address_bus($a):
403+
($a.busType // $a.bus_type // $a.adapter_type // $a.device_bus // "" | tostring | ascii_downcase);
404+
405+
def address_unit($a; $fallback):
406+
(($a.index // $a.deviceIndex // $a.device_index // $a.deviceIndex // $fallback) | tonumber? // $fallback);
407+
408+
def controller_rank($type):
409+
($type | tostring | ascii_downcase) as $t
410+
| if $t == "scsi" then 0
411+
elif $t == "virtio" then 1
412+
elif $t == "sata" then 2
413+
elif $t == "ide" then 3
414+
elif $t == "pci" then 4
415+
else 9 end;
416+
417+
def boot_disk_address($r):
418+
($r.bootConfig.bootDevice.diskAddress
419+
// $r.bootConfig.boot_device.disk_address
420+
// $r.boot_config.bootDevice.diskAddress
421+
// $r.boot_config.boot_device.disk_address
422+
// $r.resources.boot_config.boot_device.disk_address
423+
// $r.status.resources.boot_config.boot_device.disk_address
424+
// {});
425+
426+
def boot_disk_match($disk; $boot):
427+
if (($boot | type) != "object") or (($boot | length) == 0) then false
428+
else
429+
(address_bus($boot)) as $boot_bus
430+
| (address_unit($boot; -1)) as $boot_unit
431+
| (($disk.controller.type // "" | tostring | ascii_downcase) == $boot_bus
432+
and (($disk.controller.unit // -2) == $boot_unit))
433+
end;
434+
402435
def nic_items($r):
403436
($r.nics
404437
// $r.nic_list
@@ -521,9 +554,11 @@ n2k_nutanix_inventory_from_raw() {
521554
522555
def normalize_disk($d; $idx):
523556
($d.diskAddress // $d.disk_address // $d.device_properties.disk_address // $d.deviceProperties.diskAddress // {}) as $a
557+
| first_nonempty([$d.name, $d.label, $a.disk_label]) as $disk_label
524558
| {
525559
disk_id: disk_id($d; $idx),
526-
label: first_nonempty([$d.name, $d.label, $a.disk_label, ("Disk " + (($idx + 1) | tostring))]),
560+
label: $disk_label,
561+
source_ordinal: $idx,
527562
device_key: first_nonempty([$d.extId, $d.ext_id, $d.uuid, $d.device_uuid, $d.vdiskUuid, $d.vdisk_uuid, $a.device_uuid, $a.vmdisk_uuid, ($idx | tostring)]),
528563
controller: {
529564
type: first_nonempty([$a.busType, $a.bus_type, $a.adapter_type, $a.device_bus, $d.busType, $d.bus_type, "scsi"]),
@@ -540,6 +575,24 @@ n2k_nutanix_inventory_from_raw() {
540575
size_bytes: disk_size($d)
541576
};
542577
578+
def ordered_disks($r):
579+
(boot_disk_address($r)) as $boot_addr
580+
| (disk_items($r) | to_entries | map(normalize_disk(.value; .key)))
581+
| sort_by([
582+
(if boot_disk_match(.; $boot_addr) then 0 else 1 end),
583+
controller_rank(.controller.type // ""),
584+
(.controller.bus // 0),
585+
(.controller.unit // 0),
586+
(.source_ordinal // 0)
587+
])
588+
| to_entries
589+
| map(
590+
. as $entry
591+
| .value
592+
| .label = (if ((.label // "") | tostring | length) > 0 then .label else ("Disk " + (($entry.key + 1) | tostring)) end)
593+
| .role = (if $entry.key == 0 then "root" else "data" end)
594+
);
595+
543596
def normalize_nic($n; $idx):
544597
{
545598
key: ($idx | tostring),
@@ -575,7 +628,7 @@ n2k_nutanix_inventory_from_raw() {
575628
guestId: first_nonempty([$r.guestCustomization.guestOs, $r.guest_customization.guest_os, $r.guestTools.guestOs, $r.guest_tools.guest_os]),
576629
guestFamily: first_nonempty([$r.guestFamily, $r.guest_family])
577630
},
578-
disks: (disk_items($r) | to_entries | map(normalize_disk(.value; .key)))
631+
disks: ordered_disks($r)
579632
}
580633
'
581634
}

lib/n2k/target_cloud.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ n2k_cloud_target_source_deploy_params_json() {
246246
| {}
247247
+ (if $cpu > 0 then {"details[0].cpuNumber": ($cpu | floor | tostring)} else {} end)
248248
+ {"details[0].cpuSpeed": $cpu_speed}
249+
+ {"details[0].io.policy": "io_uring"}
250+
+ {"details[0].iothreads": "true"}
249251
+ (if $memory_mb > 0 then {"details[0].memory": ($memory_mb | floor | tostring)} else {} end)
250252
+ (if $root_size_gib > 0 then {"details[0].rootdisksize": ($root_size_gib | tostring)} else {} end)
251253
+ (if ($source_mac | length) > 0 then {macaddress:$source_mac} else {} end)

lib/v2k/target_cloud.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ v2k_cloud_target_source_deploy_params_json() {
228228
| {}
229229
+ (if $cpu > 0 then {"details[0].cpuNumber": ($cpu | floor | tostring)} else {} end)
230230
+ {"details[0].cpuSpeed": $cpu_speed}
231+
+ {"details[0].io.policy": "io_uring"}
232+
+ {"details[0].iothreads": "true"}
231233
+ (if $memory_mb > 0 then {"details[0].memory": ($memory_mb | floor | tostring)} else {} end)
232234
+ (if $root_size_gib > 0 then {"details[0].rootdisksize": ($root_size_gib | tostring)} else {} end)
233235
+ (if ($root_controller | length) > 0 then {"details[0].rootDiskController": $root_controller} else {} end)

lib/v2k/vmware_govc.sh

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,28 @@ v2k_vmware_inventory_json() {
394394
})
395395
);
396396
397+
def controller_rank($type; $ctrl_label):
398+
(($ctrl_label // "") | tostring | ascii_downcase) as $lbl
399+
| (($type // "") | tostring) as $typ
400+
| if ($lbl | test("^scsi controller")) or ($typ | test("SCSIController$|LsiLogic|ParaVirtualSCSI|BusLogic")) then 0
401+
elif ($lbl | test("^sata controller")) or ($typ | test("SATA"; "i")) then 1
402+
elif ($lbl | test("^nvme controller")) or ($typ | test("NVME|NVMe|Nvme")) then 2
403+
else 9 end;
404+
405+
def boot_disk_device_keys:
406+
(BOOT.bootOrder // BOOT.boot_order // [])
407+
| map((.deviceKey // .device_key // .key // empty) | tostring);
408+
409+
def boot_rank($disk; $boot_keys):
410+
([$boot_keys | to_entries[]? | select(.value == (($disk.device_key // "") | tostring)) | .key] | .[0] // 9999);
411+
397412
def disks($ctls):
398413
(DEVICES
399-
| map(select(.type=="VirtualDisk"))
414+
| to_entries
415+
| map(select(.value.type=="VirtualDisk"))
400416
| map(
401-
. as $d
417+
. as $entry
418+
| .value as $d
402419
| ($ctls | map(select(.key==$d.controllerKey)) | .[0]) as $c
403420
| {
404421
disk_id: (
@@ -413,6 +430,7 @@ v2k_vmware_inventory_json() {
413430
end
414431
),
415432
label: ($d.deviceInfo?.label // $d.label // "VirtualDisk"),
433+
source_ordinal: $entry.key,
416434
device_key: ($d.key|tostring),
417435
controller: (
418436
if $c!=null then
@@ -425,6 +443,16 @@ v2k_vmware_inventory_json() {
425443
size_bytes: ($d.capacityInBytes // 0)
426444
}
427445
)
446+
| boot_disk_device_keys as $boot_keys
447+
| sort_by([
448+
boot_rank(.; $boot_keys),
449+
controller_rank(.controller.type // ""; .controller.label // ""),
450+
(.controller.bus // 0),
451+
(.controller.unit // 0),
452+
(.source_ordinal // 0)
453+
])
454+
| to_entries
455+
| map(.value + {role:(if .key == 0 then "root" else "data" end)})
428456
);
429457
430458
{

tests/n2k_cloud_target_runtime_smoke.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ params="$(n2k_cloud_target_source_deploy_params_json "${manifest}")"
6262
jq -e '
6363
.["details[0].cpuNumber"] == "2"
6464
and .["details[0].cpuSpeed"] == "1000"
65+
and .["details[0].io.policy"] == "io_uring"
66+
and .["details[0].iothreads"] == "true"
6567
and .["details[0].memory"] == "4096"
6668
and .["details[0].rootdisksize"] == "30"
6769
and .["details[0].rootDiskController"] == "scsi"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
6+
require_cmd() {
7+
command -v "$1" >/dev/null 2>&1 || {
8+
echo "[ERR] Missing command: $1" >&2
9+
exit 2
10+
}
11+
}
12+
13+
require_cmd jq
14+
15+
# shellcheck source=/dev/null
16+
source "${ROOT_DIR}/lib/n2k/nutanix_api.sh"
17+
18+
inventory_fallback="$(n2k_nutanix_inventory_from_raw '{
19+
"name": "out-of-order",
20+
"powerState": "OFF",
21+
"disks": [
22+
{
23+
"extId": "unit-1",
24+
"sizeBytes": 1073741824000,
25+
"diskAddress": {"adapter_type": "SCSI", "device_index": 1}
26+
},
27+
{
28+
"extId": "unit-0",
29+
"sizeBytes": 536870912000,
30+
"diskAddress": {"adapter_type": "SCSI", "device_index": 0}
31+
}
32+
]
33+
}' "out-of-order")"
34+
35+
jq -e '
36+
.disks[0].disk_id == "unit-0"
37+
and .disks[0].controller.unit == 0
38+
and .disks[0].label == "Disk 1"
39+
and .disks[0].role == "root"
40+
and .disks[1].disk_id == "unit-1"
41+
and .disks[1].controller.unit == 1
42+
and .disks[1].label == "Disk 2"
43+
and .disks[1].role == "data"
44+
' <<<"${inventory_fallback}" >/dev/null || {
45+
echo "[ERR] Nutanix disk inventory was not ordered by controller unit fallback" >&2
46+
printf '%s\n' "${inventory_fallback}" >&2
47+
exit 1
48+
}
49+
50+
inventory_boot="$(n2k_nutanix_inventory_from_raw '{
51+
"name": "boot-address",
52+
"powerState": "OFF",
53+
"bootConfig": {
54+
"bootDevice": {
55+
"diskAddress": {"busType": "SCSI", "index": 1}
56+
}
57+
},
58+
"disks": [
59+
{
60+
"extId": "unit-0",
61+
"name": "Data disk",
62+
"sizeBytes": 536870912000,
63+
"diskAddress": {"busType": "SCSI", "index": 0}
64+
},
65+
{
66+
"extId": "unit-1",
67+
"name": "Boot disk",
68+
"sizeBytes": 1073741824000,
69+
"diskAddress": {"busType": "SCSI", "index": 1}
70+
}
71+
]
72+
}' "boot-address")"
73+
74+
jq -e '
75+
.disks[0].disk_id == "unit-1"
76+
and .disks[0].label == "Boot disk"
77+
and .disks[0].role == "root"
78+
and .disks[1].disk_id == "unit-0"
79+
and .disks[1].label == "Data disk"
80+
and .disks[1].role == "data"
81+
' <<<"${inventory_boot}" >/dev/null || {
82+
echo "[ERR] Nutanix explicit boot disk address did not take precedence" >&2
83+
printf '%s\n' "${inventory_boot}" >&2
84+
exit 1
85+
}
86+
87+
echo "[OK] n2k Nutanix inventory disk ordering passed"

tests/v2k_compat_installer_runtime_smoke.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ EOF
295295
jq -e '
296296
.["details[0].cpuNumber"] == "2"
297297
and .["details[0].cpuSpeed"] == "1000"
298+
and .["details[0].io.policy"] == "io_uring"
299+
and .["details[0].iothreads"] == "true"
298300
and .["details[0].memory"] == "4096"
299301
and .["details[0].rootdisksize"] == "30"
300302
and .["details[0].rootDiskController"] == "scsi"

0 commit comments

Comments
 (0)