Skip to content

Commit 32eca99

Browse files
committed
MGMT-20756: untested lsblk --json for partition handling
This is a proposed diff to solve MGMT-20756, I did not test it, and I don't have a system to test it on, so if you're working on MGMT-20756 feel free to take it and do whatever you want with it.
1 parent 346daa4 commit 32eca99

File tree

2 files changed

+260
-32
lines changed

2 files changed

+260
-32
lines changed

src/ops/ops.go

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,11 @@ func (o *ops) findEfiDirectory(device string) (string, error) {
424424
err error
425425
)
426426
if err = utils.Retry(3, 5*time.Second, o.log, func() (err error) {
427-
_, err = o.ExecPrivilegeCommand(nil, "mount", partitionForDevice(device, "2"), "/mnt")
427+
partitionPath, err := o.partitionForDevice(device, 2)
428+
if err != nil {
429+
return errors.Wrapf(err, "failed to get partition for device %s", device)
430+
}
431+
_, err = o.ExecPrivilegeCommand(nil, "mount", partitionPath, "/mnt")
428432
return
429433
}); err != nil {
430434
return "", errors.Wrap(err, "failed to mount efi device")
@@ -1029,59 +1033,125 @@ func (o *ops) ignitionPlatformId() string {
10291033
return id
10301034
}
10311035

1036+
type lsblkBlockDevice struct {
1037+
Name string `json:"name"`
1038+
Type string `json:"type"`
1039+
Size int64 `json:"size"`
1040+
Children []*lsblkBlockDevice `json:"children"`
1041+
}
1042+
1043+
type lsblkOutput struct {
1044+
Blockdevices []*lsblkBlockDevice `json:"blockdevices"`
1045+
}
1046+
10321047
func stripDev(device string) string {
10331048
return strings.Replace(device, "/dev/", "", 1)
10341049
}
10351050

1036-
func partitionNameForDeviceName(deviceName, partitionNumber string) string {
1037-
var format string
1038-
switch {
1039-
case strings.HasPrefix(deviceName, "nvme"):
1040-
format = "%sp%s"
1041-
case strings.HasPrefix(deviceName, "mmcblk"):
1042-
format = "%sP%s"
1043-
default:
1044-
format = "%s%s"
1051+
func findDeviceInLsblkOutput(device string, devices []*lsblkBlockDevice) (*lsblkBlockDevice, error) {
1052+
for _, blockDev := range devices {
1053+
if blockDev.Name == device {
1054+
return blockDev, nil
1055+
}
10451056
}
1046-
return fmt.Sprintf(format, deviceName, partitionNumber)
1057+
1058+
// Now recurse on Children
1059+
for _, blockDev := range devices {
1060+
if blockDev.Children == nil {
1061+
continue
1062+
}
1063+
1064+
if found, err := findDeviceInLsblkOutput(device, blockDev.Children); err == nil {
1065+
return found, nil
1066+
}
1067+
}
1068+
1069+
return nil, errors.Errorf("device %s not found in lsblk output", device)
10471070
}
10481071

1049-
func partitionForDevice(device, partitionNumber string) string {
1050-
return "/dev/" + partitionNameForDeviceName(stripDev(device), partitionNumber)
1072+
func getPartitionInLsblkOutput(deviceName string, partitionNumber int, lsblkJsonOutRaw string) (string, error) {
1073+
if partitionNumber < 1 {
1074+
return "", errors.Errorf("partition number must be greater than 0, got %d", partitionNumber)
1075+
}
1076+
1077+
// The partition number is 1-based, so we need to subtract 1 to get the correct index
1078+
partitionIndex := partitionNumber - 1
1079+
1080+
var output lsblkOutput
1081+
if err := json.Unmarshal([]byte(lsblkJsonOutRaw), &output); err != nil {
1082+
return "", errors.Wrapf(err, "failed to parse lsblk JSON output for device %s", deviceName)
1083+
}
1084+
1085+
dev, err := findDeviceInLsblkOutput(deviceName, output.Blockdevices)
1086+
if err != nil {
1087+
return "", errors.Wrapf(err, "failed to find device %s in lsblk output", deviceName)
1088+
}
1089+
1090+
partitionChildren := funk.Filter(dev.Children, func(child *lsblkBlockDevice) bool {
1091+
return child.Type == "part"
1092+
}).([]*lsblkBlockDevice)
1093+
1094+
if len(partitionChildren) <= partitionIndex {
1095+
return "", errors.Errorf("device %s has less than %d partitions", deviceName, partitionNumber)
1096+
}
1097+
1098+
return partitionChildren[partitionIndex].Name, nil
10511099
}
10521100

1053-
func (o *ops) calculateFreePercent(device string) (int64, error) {
1054-
type node struct {
1055-
Name string
1056-
Size int64
1057-
Children []*node
1101+
func (o *ops) partitionNameForDeviceName(deviceName string, partitionNumber int) (string, error) {
1102+
devPath := fmt.Sprintf("/dev/%s", deviceName)
1103+
// We use --bytes because we want to share structs with calculateFreePercent which expects
1104+
// lsblk sizes in bytes. If you don't do that, size can be displayed as a human readable string which
1105+
// will cause an error when unmarshalling the JSON.
1106+
lsblkOutputRaw, err := o.ExecPrivilegeCommand(nil, "lsblk", "--bytes", "--json")
1107+
if err != nil {
1108+
return "", errors.Wrapf(err, "failed to run lsblk on device %s", devPath)
10581109
}
1059-
var disks struct {
1060-
Blockdevices []*node
1110+
1111+
partitionName, err := getPartitionInLsblkOutput(deviceName, partitionNumber, lsblkOutputRaw)
1112+
if err != nil {
1113+
return "", errors.Wrapf(err, "failed to get partition name for device %s", devPath)
1114+
}
1115+
1116+
return partitionName, nil
1117+
}
1118+
1119+
func (o *ops) partitionForDevice(device string, partitionNumber int) (string, error) {
1120+
partitionName, err := o.partitionNameForDeviceName(stripDev(device), partitionNumber)
1121+
if err != nil {
1122+
return "", errors.Wrapf(err, "failed to get partition name for device %s", device)
10611123
}
1124+
return fmt.Sprintf("/dev/%s", partitionName), nil
1125+
}
1126+
1127+
func (o *ops) calculateFreePercent(device string) (int64, error) {
1128+
var disks lsblkOutput
10621129
var (
1063-
diskNode, partitionNode *node
1130+
diskNode, partitionNode *lsblkBlockDevice
10641131
ok bool
10651132
)
1066-
ret, err := o.ExecPrivilegeCommand(nil, "lsblk", "-b", "-J")
1133+
ret, err := o.ExecPrivilegeCommand(nil, "lsblk", "--bytes", "--json")
10671134
if err != nil {
10681135
return 0, errors.Wrap(err, "failed to run lsblk command")
10691136
}
10701137
if err = json.Unmarshal([]byte(ret), &disks); err != nil {
10711138
return 0, errors.Wrap(err, "failed to unmarshal lsblk output")
10721139
}
10731140
deviceName := stripDev(device)
1074-
diskNode, ok = funk.Find(disks.Blockdevices, func(n *node) bool { return deviceName == n.Name }).(*node)
1141+
diskNode, ok = funk.Find(disks.Blockdevices, func(n *lsblkBlockDevice) bool { return deviceName == n.Name }).(*lsblkBlockDevice)
10751142
if !ok {
10761143
return 0, errors.Errorf("failed to find device is %s in lsblk output", device)
10771144
}
1078-
partitionName := partitionNameForDeviceName(diskNode.Name, "4")
1079-
partitionNode, ok = funk.Find(diskNode.Children, func(n *node) bool { return partitionName == n.Name }).(*node)
1145+
partitionName, err := o.partitionNameForDeviceName(diskNode.Name, 4)
1146+
if err != nil {
1147+
return 0, errors.Wrapf(err, "failed to find partition name for device %s", diskNode.Name)
1148+
}
1149+
partitionNode, ok = funk.Find(diskNode.Children, func(n *lsblkBlockDevice) bool { return partitionName == n.Name }).(*lsblkBlockDevice)
10801150
if !ok {
10811151
return 0, errors.Errorf("failed to find partition node %s in lsblk output", device)
10821152
}
10831153
var usedSize int64
1084-
funk.ForEach(diskNode.Children, func(n *node) { usedSize += n.Size })
1154+
funk.ForEach(diskNode.Children, func(n *lsblkBlockDevice) { usedSize += n.Size })
10851155

10861156
// The assumption is that the extra space needed for image overwrite is not more than the existing partition size. So
10871157
// the partition size will be doubled, and the rest will remain as free space.
@@ -1116,9 +1186,18 @@ func (o *ops) OverwriteOsImage(osImage, device string, extraArgs []string) error
11161186
o.log.Infof("Running on s390x archtiecture - skip overwrite image.")
11171187
return nil
11181188
}
1189+
mntPartition, err := o.partitionForDevice(device, 4)
1190+
if err != nil {
1191+
return errors.Wrapf(err, "failed to get fourth partition for device %s", device)
1192+
}
1193+
1194+
mntBootPartition, err := o.partitionForDevice(device, 3)
1195+
if err != nil {
1196+
return errors.Wrapf(err, "failed to get third (boot) partition for device %s", device)
1197+
}
11191198
cmds := []*cmd{
1120-
makecmd("mount", partitionForDevice(device, "4"), "/mnt"),
1121-
makecmd("mount", partitionForDevice(device, "3"), "/mnt/boot"),
1199+
makecmd("mount", mntPartition, "/mnt"),
1200+
makecmd("mount", mntBootPartition, "/mnt/boot"),
11221201
growpartcmd,
11231202
makecmd("xfs_growfs", "/mnt"),
11241203

src/ops/ops_test.go

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"os"
1313
"reflect"
14+
"strings"
1415

1516
"github.com/openshift/assisted-installer/src/ops/execute"
1617
"github.com/openshift/assisted-installer/src/utils"
@@ -456,13 +457,23 @@ var _ = Describe("overwrite OS image", func() {
456457
"--pid",
457458
"--"), args...)...).Times(1)
458459
}
460+
formatPartitionName := func(deviceName string, partNum int) string {
461+
// Generate partition names following the naming convention
462+
if strings.HasPrefix(deviceName, "nvme") || strings.HasPrefix(deviceName, "mmcblk") {
463+
if strings.HasPrefix(deviceName, "mmcblk") {
464+
return fmt.Sprintf("%sP%d", deviceName, partNum)
465+
}
466+
return fmt.Sprintf("%sp%d", deviceName, partNum)
467+
}
468+
return fmt.Sprintf("%s%d", deviceName, partNum)
469+
}
459470
formatResult := func(device string) string {
460471
deviceName := stripDev(device)
461472
return fmt.Sprintf(lsblkResultFormat, deviceName,
462-
partitionNameForDeviceName(deviceName, "1"),
463-
partitionNameForDeviceName(deviceName, "2"),
464-
partitionNameForDeviceName(deviceName, "3"),
465-
partitionNameForDeviceName(deviceName, "4"))
473+
formatPartitionName(deviceName, 1),
474+
formatPartitionName(deviceName, 2),
475+
formatPartitionName(deviceName, 3),
476+
formatPartitionName(deviceName, 4))
466477
}
467478
runTest := func(device, part3, part4 string) {
468479
execMock.EXPECT().ExecCommand(nil, "nsenter",
@@ -575,6 +586,144 @@ var _ = Describe("get number of reboots", func() {
575586
})
576587
})
577588

589+
var _ = Describe("getPartitionInLsblkOutput", func() {
590+
const testLsblkJsonOutput = `{
591+
"blockdevices": [
592+
{
593+
"name": "sda",
594+
"type": "disk",
595+
"size": 275200000000
596+
},{
597+
"name": "nvme1n1",
598+
"type": "disk",
599+
"size": 1980000000000,
600+
"children": [
601+
{
602+
"name": "nvme1n1p1",
603+
"type": "part",
604+
"size": 512000000
605+
},{
606+
"name": "nvme1n1p2",
607+
"type": "part",
608+
"size": 1073741824
609+
},{
610+
"name": "nvme1n1p3",
611+
"type": "part",
612+
"size": 1900000000000,
613+
"children": [
614+
{
615+
"name": "fedora-00",
616+
"type": "lvm",
617+
"size": 1900000000000
618+
}
619+
]
620+
}
621+
]
622+
},{
623+
"name": "nvme0n1",
624+
"type": "disk",
625+
"size": 1980000000000,
626+
"children": [
627+
{
628+
"name": "nvme0n1p1",
629+
"type": "part",
630+
"size": 16777216
631+
},{
632+
"name": "nvme0n1p2",
633+
"type": "part",
634+
"size": 1900000000000
635+
},{
636+
"name": "nvme0n1p3",
637+
"type": "part",
638+
"size": 763000000
639+
}
640+
]
641+
}
642+
]
643+
}`
644+
645+
It("should return correct partition name for valid inputs", func() {
646+
partitionName, err := getPartitionInLsblkOutput("nvme1n1", 1, testLsblkJsonOutput)
647+
Expect(err).ToNot(HaveOccurred())
648+
Expect(partitionName).To(Equal("nvme1n1p1"))
649+
650+
partitionName, err = getPartitionInLsblkOutput("nvme1n1", 2, testLsblkJsonOutput)
651+
Expect(err).ToNot(HaveOccurred())
652+
Expect(partitionName).To(Equal("nvme1n1p2"))
653+
654+
partitionName, err = getPartitionInLsblkOutput("nvme1n1", 3, testLsblkJsonOutput)
655+
Expect(err).ToNot(HaveOccurred())
656+
Expect(partitionName).To(Equal("nvme1n1p3"))
657+
})
658+
659+
It("should work with different device types", func() {
660+
partitionName, err := getPartitionInLsblkOutput("nvme0n1", 1, testLsblkJsonOutput)
661+
Expect(err).ToNot(HaveOccurred())
662+
Expect(partitionName).To(Equal("nvme0n1p1"))
663+
664+
partitionName, err = getPartitionInLsblkOutput("nvme0n1", 2, testLsblkJsonOutput)
665+
Expect(err).ToNot(HaveOccurred())
666+
Expect(partitionName).To(Equal("nvme0n1p2"))
667+
668+
partitionName, err = getPartitionInLsblkOutput("nvme0n1", 3, testLsblkJsonOutput)
669+
Expect(err).ToNot(HaveOccurred())
670+
Expect(partitionName).To(Equal("nvme0n1p3"))
671+
})
672+
673+
It("should return error for partition number less than 1", func() {
674+
_, err := getPartitionInLsblkOutput("nvme1n1", 0, testLsblkJsonOutput)
675+
Expect(err).To(HaveOccurred())
676+
Expect(err.Error()).To(ContainSubstring("partition number must be greater than 0"))
677+
678+
_, err = getPartitionInLsblkOutput("nvme1n1", -1, testLsblkJsonOutput)
679+
Expect(err).To(HaveOccurred())
680+
Expect(err.Error()).To(ContainSubstring("partition number must be greater than 0"))
681+
})
682+
683+
It("should return error for device not found", func() {
684+
_, err := getPartitionInLsblkOutput("nonexistent", 1, testLsblkJsonOutput)
685+
Expect(err).To(HaveOccurred())
686+
Expect(err.Error()).To(ContainSubstring("device nonexistent not found in lsblk output"))
687+
})
688+
689+
It("should return error for partition number exceeding available partitions", func() {
690+
_, err := getPartitionInLsblkOutput("nvme1n1", 4, testLsblkJsonOutput)
691+
Expect(err).To(HaveOccurred())
692+
Expect(err.Error()).To(ContainSubstring("device nvme1n1 has less than 4 partitions"))
693+
694+
_, err = getPartitionInLsblkOutput("nvme0n1", 4, testLsblkJsonOutput)
695+
Expect(err).To(HaveOccurred())
696+
Expect(err.Error()).To(ContainSubstring("device nvme0n1 has less than 4 partitions"))
697+
})
698+
699+
It("should return error for device with no partitions", func() {
700+
_, err := getPartitionInLsblkOutput("sda", 1, testLsblkJsonOutput)
701+
Expect(err).To(HaveOccurred())
702+
Expect(err.Error()).To(ContainSubstring("device sda has less than 1 partitions"))
703+
})
704+
705+
It("should return error for invalid JSON", func() {
706+
invalidJson := `{"blockdevices": [invalid json}`
707+
_, err := getPartitionInLsblkOutput("nvme1n1", 1, invalidJson)
708+
Expect(err).To(HaveOccurred())
709+
Expect(err.Error()).To(ContainSubstring("failed to parse lsblk JSON output"))
710+
})
711+
712+
It("should return error for empty JSON", func() {
713+
emptyJson := `{"blockdevices": []}`
714+
_, err := getPartitionInLsblkOutput("nvme1n1", 1, emptyJson)
715+
Expect(err).To(HaveOccurred())
716+
Expect(err.Error()).To(ContainSubstring("device nvme1n1 not found in lsblk output"))
717+
})
718+
719+
It("should ignore non-partition children", func() {
720+
// nvme1n1p3 has an LVM child, but getPartitionInLsblkOutput should only look at "part" type children
721+
partitionName, err := getPartitionInLsblkOutput("nvme1n1", 3, testLsblkJsonOutput)
722+
Expect(err).ToNot(HaveOccurred())
723+
Expect(partitionName).To(Equal("nvme1n1p3"))
724+
})
725+
})
726+
578727
var _ = Describe("WriteImageToExistingRoot", func() {
579728
const (
580729
osImage = "quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:d21f2ed754a66d18b0a13a59434fa4dc36abd4320e78f3be83a3e29e21e3c2f9"

0 commit comments

Comments
 (0)