From 902b093a8e8c256fe8bbac369e1dce808ba39ba1 Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Mon, 26 Aug 2024 17:17:31 -0400 Subject: [PATCH 1/4] distro: add sfdisk cmd --- internal/distro/distro.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/distro/distro.go b/internal/distro/distro.go index cd90d1896..11bd4fe7b 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -38,6 +38,7 @@ var ( mountCmd = "mount" partxCmd = "partx" sgdiskCmd = "sgdisk" + sfdiskCmd = "sfdisk" modprobeCmd = "modprobe" udevadmCmd = "udevadm" usermodCmd = "usermod" @@ -95,6 +96,7 @@ func MdadmCmd() string { return mdadmCmd } func MountCmd() string { return mountCmd } func PartxCmd() string { return partxCmd } func SgdiskCmd() string { return sgdiskCmd } +func SfdiskCmd() string { return sfdiskCmd } func ModprobeCmd() string { return modprobeCmd } func UdevadmCmd() string { return udevadmCmd } func UsermodCmd() string { return usermodCmd } From 322626aabe115564b2930be3369ad7f072b12f4a Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Mon, 26 Aug 2024 17:18:17 -0400 Subject: [PATCH 2/4] sfdisk: add sfdisk module with 'pretend' support This is a copy of sgdisk, with mapping around sfdisk tooling. Update the buildOptions to understand sfdisk requirements and focus on adding support for the pretend function. Fixes: https://issues.redhat.com/browse/COS-2837 --- config/shared/errors/errors.go | 2 + internal/exec/stages/disks/partitions.go | 61 +++- internal/exec/stages/disks/partitions_test.go | 106 +++++++ internal/sfdisk/sfdisk.go | 276 ++++++++++++++++++ 4 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 internal/exec/stages/disks/partitions_test.go create mode 100644 internal/sfdisk/sfdisk.go diff --git a/config/shared/errors/errors.go b/config/shared/errors/errors.go index 85d110112..a52a52a44 100644 --- a/config/shared/errors/errors.go +++ b/config/shared/errors/errors.go @@ -87,6 +87,8 @@ var ( ErrPathConflictsSystemd = errors.New("path conflicts with systemd unit or dropin") ErrCexWithClevis = errors.New("cannot use cex with clevis") ErrCexWithKeyFile = errors.New("cannot use key file with cex") + ErrBadSfdiskPretend = errors.New("sfdisk had unexpected output while pretending partition configuration on device") + ErrBadSfdiskCommit = errors.New("sfdisk had unexpected output while committing partition configuration to device") // Systemd section errors ErrInvalidSystemdExt = errors.New("invalid systemd unit extension") diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go index 6285fd5c4..36bb3881b 100644 --- a/internal/exec/stages/disks/partitions.go +++ b/internal/exec/stages/disks/partitions.go @@ -30,6 +30,7 @@ import ( "strconv" "strings" + sharedErrors "github.com/coreos/ignition/v2/config/shared/errors" cutil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/internal/distro" @@ -177,7 +178,7 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti return nil, err } - realDimensions, err := parseSgdiskPretend(output, partitionsToInspect) + realDimensions, err := parseSfdiskPretend(output, partitionsToInspect) if err != nil { return nil, err } @@ -284,6 +285,64 @@ func parseSgdiskPretend(sgdiskOut string, partitionNumbers []int) (map[int]sgdis return output, nil } +type sfdiskOutput struct { + start int64 + size int64 +} + +// ParsePretend takes the output from sfdisk running with the argument --no-act. Similar to sgdisk +// it then uses regex to parse the output into understood values like 'start' 'size' and attempts +// to catch any failures and wrap them to return to the caller. +func parseSfdiskPretend(sfdiskOut string, partitionNumbers []int) (map[int]sfdiskOutput, error) { + if len(partitionNumbers) == 0 { + return nil, nil + } + + // Prepare the data, and a regex for matching on partitions + partitionRegex := regexp.MustCompile(`^/dev/\S+\s+\S*\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+\S+\s+\S+.*$`) + output := map[int]sfdiskOutput{} + current := sfdiskOutput{} + i := 0 + lines := strings.Split(sfdiskOut, "\n") + for _, line := range lines { + matches := partitionRegex.FindStringSubmatch(line) + + // Sanity check number of partition entries + if i > len(partitionNumbers) { + return nil, sharedErrors.ErrBadSfdiskPretend + } + + // Verify that we are not reading a 'failed' or 'error' + errorRegex := regexp.MustCompile(`(?i)(failed|error)`) + if errorRegex.MatchString(line) { + return nil, fmt.Errorf("%w: sfdisk returned :%v", sharedErrors.ErrBadSfdiskPretend, line) + } + + // When we get a match it should be + // Whole line at [0] + // Start at [1] + // Size at [2] + if len(matches) > 1 { + start, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + end, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, err + } + + current.start = int64(start) + // Add one due to overlap + current.size = int64(end - start + 1) + output[partitionNumbers[i]] = current + i++ + } + } + + return output, nil +} + // partitionShouldExist returns whether a bool is indicating if a partition should exist or not. // nil (unspecified in json) is treated the same as true. func partitionShouldExist(part sgdisk.Partition) bool { diff --git a/internal/exec/stages/disks/partitions_test.go b/internal/exec/stages/disks/partitions_test.go new file mode 100644 index 000000000..683cb044e --- /dev/null +++ b/internal/exec/stages/disks/partitions_test.go @@ -0,0 +1,106 @@ +package disks + +import ( + "errors" + "reflect" + "testing" + + internalErrors "github.com/coreos/ignition/v2/config/shared/errors" +) + +func TestPartitionParse(t *testing.T) { + // Define test cases + tests := []struct { + name string + sfdiskOut string + partitionNumbers []int + expectedOutput map[int]sfdiskOutput + expectedError error + }{ + { + name: "valid input with single partition", + sfdiskOut: ` +Disk /dev/vda: 2 GiB, 2147483648 bytes, 4194304 sectors +Units: sectors of 1 * 512 = 512 bytes +Sector size (logical/physical): 512 bytes / 512 bytes +I/O size (minimum/optimal): 512 bytes / 512 bytes + +>>> Created a new DOS (MBR) disklabel with disk identifier 0x501fc254. +/dev/vda1: Created a new partition 1 of type 'Linux' and of size 5 KiB. +/dev/vda2: Done. + +New situation: +Disklabel type: dos +Disk identifier: 0x501fc254 + +Device Boot Start End Sectors Size Id Type +/dev/vda1 2048 2057 10 5K 83 Linux +The partition table is unchanged (--no-act).`, + partitionNumbers: []int{1}, + expectedOutput: map[int]sfdiskOutput{ + 1: {start: 2048, size: 10}, + }, + expectedError: nil, + }, + { + name: "valid input with two partitions", + sfdiskOut: ` +Disk /dev/vda: 2 GiB, 2147483648 bytes, 4194304 sectors +Units: sectors of 1 * 512 = 512 bytes +Sector size (logical/physical): 512 bytes / 512 bytes +I/O size (minimum/optimal): 512 bytes / 512 bytes + +>>> Created a new DOS (MBR) disklabel with disk identifier 0x8d8dd38c. +/dev/vda1: Created a new partition 1 of type 'Linux' and of size 5 KiB. +/dev/vda2: Created a new partition 2 of type 'Linux' and of size 5 KiB. +/dev/vda3: Done. + +New situation: +Disklabel type: dos +Disk identifier: 0x8d8dd38c + +Device Boot Start End Sectors Size Id Type +/dev/vda1 2048 2057 10 5K 83 Linux +/dev/vda2 4096 4105 10 5K 83 Linux +The partition table is unchanged (--no-act).`, + partitionNumbers: []int{1, 2}, + expectedOutput: map[int]sfdiskOutput{ + 1: {start: 2048, size: 10}, + 2: {start: 4096, size: 10}, + }, + expectedError: nil, + }, + { + name: "invalid input with 1 partition starting on sector 0", + sfdiskOut: ` +Disk /dev/vda: 2 GiB, 2147483648 bytes, 4194304 sectors +Units: sectors of 1 * 512 = 512 bytes +Sector size (logical/physical): 512 bytes / 512 bytes +I/O size (minimum/optimal): 512 bytes / 512 bytes + +>>> Created a new DOS (MBR) disklabel with disk identifier 0xdebbe997. +/dev/vda1: Start sector 0 out of range. +Failed to add #1 partition: Numerical result out of range +Leaving. +`, + partitionNumbers: []int{1}, + expectedOutput: map[int]sfdiskOutput{ + 1: {start: 0, size: 0}, + }, + expectedError: internalErrors.ErrBadSfdiskPretend, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := parseSfdiskPretend(tt.sfdiskOut, tt.partitionNumbers) + if tt.expectedError != nil { + if !errors.Is(err, tt.expectedError) { + t.Errorf("#%d: bad error: result = %v, expected = %v", i, err, tt.expectedError) + } + } else if !reflect.DeepEqual(output, tt.expectedOutput) { + t.Errorf("#%d: result = %v, expected = %v", i, output, tt.expectedOutput) + } + }) + } +} diff --git a/internal/sfdisk/sfdisk.go b/internal/sfdisk/sfdisk.go new file mode 100644 index 000000000..37c16d121 --- /dev/null +++ b/internal/sfdisk/sfdisk.go @@ -0,0 +1,276 @@ +// Copyright 2024 Red Hat +// +// Licensed 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. + +package sfdisk + +import ( + "bytes" + "fmt" + "io" + "os/exec" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + "github.com/coreos/ignition/v2/internal/distro" + "github.com/coreos/ignition/v2/internal/log" +) + +type Operation struct { + logger *log.Logger + dev string + wipe bool + parts []Partition + deletions []int + infos []int +} + +// We ignore types.Partition.StartMiB/SizeMiB in favor of +// StartSector/SizeInSectors. The caller is expected to do the conversion. +type Partition struct { + types.Partition + StartSector *int64 + SizeInSectors *int64 + + // shadow StartMiB/SizeMiB so they're not accidentally used + StartMiB string + SizeMiB string +} + +// Begin begins an sfdisk operation +func Begin(logger *log.Logger, dev string) *Operation { + return &Operation{logger: logger, dev: dev} +} + +// CreatePartition adds the supplied partition to the list of partitions to be created as part of an operation. +func (op *Operation) CreatePartition(p Partition) { + op.parts = append(op.parts, p) +} + +func (op *Operation) DeletePartition(num int) { + op.deletions = append(op.deletions, num) +} + +func (op *Operation) Info(num int) { + op.infos = append(op.infos, num) +} + +// WipeTable toggles if the table is to be wiped first when committing this operation. +func (op *Operation) WipeTable(wipe bool) { + op.wipe = wipe +} + +// Pretend is like Commit() but uses the --no-act flag and returns the output +func (op *Operation) Pretend() (string, error) { + // Handle deletions first + if err := op.handleDeletions(); err != nil { + return "", err + } + + if err := op.handleInfo(); err != nil { + return "", err + } + + script := op.sfdiskBuildOptions() + cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -e \"%s\" | sudo %s --no-act %s", script, distro.SfdiskCmd(), op.dev)) + stdout, err := cmd.StdoutPipe() + + if err != nil { + return "", err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return "", err + } + + if err := cmd.Start(); err != nil { + return "", err + } + output, err := io.ReadAll(stdout) + if err != nil { + return "", err + } + + errors, err := io.ReadAll(stderr) + if err != nil { + return "", err + } + + if err := cmd.Wait(); err != nil { + return "", fmt.Errorf("failed to pretend to create partitions. Err: %v. Stderr: %v", err, string(errors)) + } + + return string(output), nil +} + +// Commit commits an partitioning operation. +func (op *Operation) SfdiskCommit() error { + script := op.sfdiskBuildOptions() + if len(script) == 0 { + return nil + } + + // If wipe we need to reset the partition table + if op.wipe { + // Erase the existing partition tables + cmd := exec.Command("sudo", distro.WipefsCmd(), "-a", op.dev) + if _, err := op.logger.LogCmd(cmd, "option wipe selected, and failed to execute on %q", op.dev); err != nil { + return fmt.Errorf("wipe partition table failed: %v", err) + } + } + + op.logger.Info("running sfdisk with script: %v", script) + exec.Command("sh", "-c", fmt.Sprintf("echo label: gpt | sudo %s %s", distro.SfdiskCmd(), op.dev)) + cmd := exec.Command("sh", "-c", fmt.Sprintf("echo \"%s\" | sudo %s %s", script, distro.SfdiskCmd(), op.dev)) + if _, err := op.logger.LogCmd(cmd, "deleting %d partitions and creating %d partitions on %q", len(op.deletions), len(op.parts), op.dev); err != nil { + return fmt.Errorf("create partitions failed: %v", err) + } + + return nil +} + +func (op Operation) sfdiskBuildOptions() string { + var script bytes.Buffer + + for _, p := range op.parts { + if p.Number != 0 { + script.WriteString(fmt.Sprintf("%d : ", p.Number)) + } + + if p.StartSector != nil { + script.WriteString(fmt.Sprintf("start=%d ", *p.StartSector)) + + } + + if p.SizeInSectors != nil { + script.WriteString(fmt.Sprintf("size=%d ", *p.SizeInSectors)) + } + + if util.NotEmpty(p.TypeGUID) { + script.WriteString(fmt.Sprintf("type=%s ", *p.TypeGUID)) + } + + if util.NotEmpty(p.GUID) { + script.WriteString(fmt.Sprintf("uuid=%s ", *p.GUID)) + } + + if p.Label != nil { + script.WriteString(fmt.Sprintf("name=%s ", *p.Label)) + } + + // Add escaped new line to allow for 1 or more partitions + // i.e "1: size=50 \\n size=10" will result in part 1, and 2 + script.WriteString("\\n ") + + } + + return script.String() +} + +func (op *Operation) handleDeletions() error { + for _, num := range op.deletions { + cmd := exec.Command(distro.SfdiskCmd(), "--delete", op.dev, fmt.Sprintf("%d", num)) + op.logger.Info("running sfdisk to delete partition %d on %q", num, op.dev) + + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to delete partition %d: %v, output: %s", num, err, output) + } + } + return nil +} + +func (op *Operation) handleInfo() error { + for _, num := range op.infos { + cmd := exec.Command(distro.SfdiskCmd(), "--list", op.dev) + op.logger.Info("retrieving information for partition %d on %q", num, op.dev) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to retrieve partition info for %d: %v, output: %s", num, err, output) + } + op.logger.Info("partition info: %s", output) + } + return nil +} + +// Copy old functionality from sgdisk to switch between the two during testing. +// Will be removed. +func (op *Operation) SgdiskCommit() error { + opts := op.sgdiskBuildOptions() + if len(opts) == 0 { + return nil + } + op.logger.Info("running sgdisk with options: %v", opts) + cmd := exec.Command(distro.SgdiskCmd(), opts...) + + if _, err := op.logger.LogCmd(cmd, "deleting %d partitions and creating %d partitions on %q", len(op.deletions), len(op.parts), op.dev); err != nil { + return fmt.Errorf("create partitions failed: %v", err) + } + + return nil +} + +// Copy old functionality from sgdisk to switch between the two during testing. +// Will be removed. +func (op Operation) sgdiskBuildOptions() []string { + opts := []string{} + + if op.wipe { + opts = append(opts, "--zap-all") + } + + // Do all deletions before creations + for _, partition := range op.deletions { + opts = append(opts, fmt.Sprintf("--delete=%d", partition)) + } + + for _, p := range op.parts { + opts = append(opts, fmt.Sprintf("--new=%d:%s:+%s", p.Number, partitionGetStart(p), partitionGetSize(p))) + if p.Label != nil { + opts = append(opts, fmt.Sprintf("--change-name=%d:%s", p.Number, *p.Label)) + } + if util.NotEmpty(p.TypeGUID) { + opts = append(opts, fmt.Sprintf("--typecode=%d:%s", p.Number, *p.TypeGUID)) + } + if util.NotEmpty(p.GUID) { + opts = append(opts, fmt.Sprintf("--partition-guid=%d:%s", p.Number, *p.GUID)) + } + } + + for _, partition := range op.infos { + opts = append(opts, fmt.Sprintf("--info=%d", partition)) + } + + if len(opts) == 0 { + return nil + } + + opts = append(opts, op.dev) + return opts +} + +// Copy old functionality from sgdisk to switch between the two during testing. +// Will be removed. +func partitionGetStart(p Partition) string { + if p.StartSector != nil { + return fmt.Sprintf("%d", *p.StartSector) + } + return "0" +} + +func partitionGetSize(p Partition) string { + if p.SizeInSectors != nil { + return fmt.Sprintf("%d", *p.SizeInSectors) + } + return "0" +} From 1709f6f56304e3b25967a96a8552af565fd38741 Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Tue, 17 Sep 2024 16:46:18 -0400 Subject: [PATCH 3/4] partitioners: move and add abstraction for sgdisk and sfdisk Add an interface to abstract implementation details for partition management, allowing tooling to pivot between implementations. TBD: argument/tag reading for triggering the pivot Fixes: https://issues.redhat.com/browse/COS-2930 --- internal/exec/stages/disks/partitions.go | 196 +++--------------- internal/partitioners/partitioners.go | 42 ++++ internal/{ => partitioners}/sfdisk/sfdisk.go | 191 +++++++++-------- .../sfdisk/sfdisk_test.go} | 38 +++- internal/{ => partitioners}/sgdisk/sgdisk.go | 112 ++++++++-- 5 files changed, 285 insertions(+), 294 deletions(-) create mode 100644 internal/partitioners/partitioners.go rename internal/{ => partitioners}/sfdisk/sfdisk.go (61%) rename internal/{exec/stages/disks/partitions_test.go => partitioners/sfdisk/sfdisk_test.go} (71%) rename internal/{ => partitioners}/sgdisk/sgdisk.go (58%) diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go index 36bb3881b..ce7edb07d 100644 --- a/internal/exec/stages/disks/partitions.go +++ b/internal/exec/stages/disks/partitions.go @@ -20,28 +20,32 @@ package disks import ( "bufio" - "errors" "fmt" "os" "os/exec" "path/filepath" - "regexp" "sort" "strconv" "strings" - sharedErrors "github.com/coreos/ignition/v2/config/shared/errors" cutil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/exec/util" - "github.com/coreos/ignition/v2/internal/sgdisk" + "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/partitioners" + "github.com/coreos/ignition/v2/internal/partitioners/sfdisk" + "github.com/coreos/ignition/v2/internal/partitioners/sgdisk" iutil "github.com/coreos/ignition/v2/internal/util" ) -var ( - ErrBadSgdiskOutput = errors.New("sgdisk had unexpected output") -) +func getDeviceManager(logger *log.Logger, dev string) partitioners.DeviceManager { + // To be replaced with build tag support or something similar. + if false { + return sgdisk.Begin(logger, dev) + } + return sfdisk.Begin(logger, dev) +} // createPartitions creates the partitions described in config.Storage.Disks. func (s stage) createPartitions(config types.Config) error { @@ -76,7 +80,7 @@ func (s stage) createPartitions(config types.Config) error { // partitionMatches determines if the existing partition matches the spec given. See doc/operator notes for what // what it means for an existing partition to match the spec. spec must have non-zero Start and Size. -func partitionMatches(existing util.PartitionInfo, spec sgdisk.Partition) error { +func partitionMatches(existing util.PartitionInfo, spec partitioners.Partition) error { if err := partitionMatchesCommon(existing, spec); err != nil { return err } @@ -88,13 +92,13 @@ func partitionMatches(existing util.PartitionInfo, spec sgdisk.Partition) error // partitionMatchesResize returns if the existing partition should be resized by evaluating if // `resize`field is true and partition matches in all respects except size. -func partitionMatchesResize(existing util.PartitionInfo, spec sgdisk.Partition) bool { +func partitionMatchesResize(existing util.PartitionInfo, spec partitioners.Partition) bool { return cutil.IsTrue(spec.Resize) && partitionMatchesCommon(existing, spec) == nil } // partitionMatchesCommon handles the common tests (excluding the partition size) to determine // if the existing partition matches the spec given. -func partitionMatchesCommon(existing util.PartitionInfo, spec sgdisk.Partition) error { +func partitionMatchesCommon(existing util.PartitionInfo, spec partitioners.Partition) error { if spec.Number != existing.Number { return fmt.Errorf("partition numbers did not match (specified %d, got %d). This should not happen, please file a bug", spec.Number, existing.Number) } @@ -114,7 +118,7 @@ func partitionMatchesCommon(existing util.PartitionInfo, spec sgdisk.Partition) } // partitionShouldBeInspected returns if the partition has zeroes that need to be resolved to sectors. -func partitionShouldBeInspected(part sgdisk.Partition) bool { +func partitionShouldBeInspected(part partitioners.Partition) bool { if part.Number == 0 { return false } @@ -134,17 +138,17 @@ func convertMiBToSectors(mib *int, sectorSize int) *int64 { // getRealStartAndSize returns a map of partition numbers to a struct that contains what their real start // and end sector should be. It runs sgdisk --pretend to determine what the partitions would look like if // everything specified were to be (re)created. -func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]sgdisk.Partition, error) { - partitions := []sgdisk.Partition{} +func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]partitioners.Partition, error) { + partitions := []partitioners.Partition{} for _, cpart := range dev.Partitions { - partitions = append(partitions, sgdisk.Partition{ + partitions = append(partitions, partitioners.Partition{ Partition: cpart, StartSector: convertMiBToSectors(cpart.StartMiB, diskInfo.LogicalSectorSize), SizeInSectors: convertMiBToSectors(cpart.SizeMiB, diskInfo.LogicalSectorSize), }) } - op := sgdisk.Begin(s.Logger, devAlias) + op := getDeviceManager(s.Logger, devAlias) for _, part := range partitions { if info, exists := diskInfo.GetPartition(part.Number); exists { // delete all existing partitions @@ -177,20 +181,19 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti if err != nil { return nil, err } - - realDimensions, err := parseSfdiskPretend(output, partitionsToInspect) + realDimensions, err := op.ParseOutput(output, partitionsToInspect) if err != nil { return nil, err } - result := []sgdisk.Partition{} + result := []partitioners.Partition{} for _, part := range partitions { if dims, ok := realDimensions[part.Number]; ok { if part.StartSector != nil { - part.StartSector = &dims.start + part.StartSector = &dims.Start } if part.SizeInSectors != nil { - part.SizeInSectors = &dims.size + part.SizeInSectors = &dims.Size } } result = append(result, part) @@ -198,154 +201,9 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti return result, nil } -type sgdiskOutput struct { - start int64 - size int64 -} - -// parseLine takes a regexp that captures an int64 and a string to match on. On success it returns -// the captured int64 and nil. If the regexp does not match it returns -1 and nil. If it encountered -// an error it returns 0 and the error. -func parseLine(r *regexp.Regexp, line string) (int64, error) { - matches := r.FindStringSubmatch(line) - switch len(matches) { - case 0: - return -1, nil - case 2: - return strconv.ParseInt(matches[1], 10, 64) - default: - return 0, ErrBadSgdiskOutput - } -} - -// parseSgdiskPretend parses the output of running sgdisk pretend with --info specified for each partition -// number specified in partitionNumbers. E.g. if paritionNumbers is [1,4,5], it is expected that the sgdisk -// output was from running `sgdisk --pretend --info=1 --info=4 --info=5`. It assumes the the -// partition labels are well behaved (i.e. contain no control characters). It returns a list of partitions -// matching the partition numbers specified, but with the start and size information as determined by sgdisk. -// The partition numbers need to passed in because sgdisk includes them in its output. -func parseSgdiskPretend(sgdiskOut string, partitionNumbers []int) (map[int]sgdiskOutput, error) { - if len(partitionNumbers) == 0 { - return nil, nil - } - startRegex := regexp.MustCompile(`^First sector: (\d*) \(.*\)$`) - endRegex := regexp.MustCompile(`^Last sector: (\d*) \(.*\)$`) - const ( - START = iota - END = iota - FAIL_ON_START_END = iota - ) - - output := map[int]sgdiskOutput{} - state := START - current := sgdiskOutput{} - i := 0 - - lines := strings.Split(sgdiskOut, "\n") - for _, line := range lines { - switch state { - case START: - start, err := parseLine(startRegex, line) - if err != nil { - return nil, err - } - if start != -1 { - current.start = start - state = END - } - case END: - end, err := parseLine(endRegex, line) - if err != nil { - return nil, err - } - if end != -1 { - current.size = 1 + end - current.start - output[partitionNumbers[i]] = current - i++ - if i == len(partitionNumbers) { - state = FAIL_ON_START_END - } else { - current = sgdiskOutput{} - state = START - } - } - case FAIL_ON_START_END: - if len(startRegex.FindStringSubmatch(line)) != 0 || - len(endRegex.FindStringSubmatch(line)) != 0 { - return nil, ErrBadSgdiskOutput - } - } - } - - if state != FAIL_ON_START_END { - // We stopped parsing in the middle of a info block. Something is wrong - return nil, ErrBadSgdiskOutput - } - - return output, nil -} - -type sfdiskOutput struct { - start int64 - size int64 -} - -// ParsePretend takes the output from sfdisk running with the argument --no-act. Similar to sgdisk -// it then uses regex to parse the output into understood values like 'start' 'size' and attempts -// to catch any failures and wrap them to return to the caller. -func parseSfdiskPretend(sfdiskOut string, partitionNumbers []int) (map[int]sfdiskOutput, error) { - if len(partitionNumbers) == 0 { - return nil, nil - } - - // Prepare the data, and a regex for matching on partitions - partitionRegex := regexp.MustCompile(`^/dev/\S+\s+\S*\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+\S+\s+\S+.*$`) - output := map[int]sfdiskOutput{} - current := sfdiskOutput{} - i := 0 - lines := strings.Split(sfdiskOut, "\n") - for _, line := range lines { - matches := partitionRegex.FindStringSubmatch(line) - - // Sanity check number of partition entries - if i > len(partitionNumbers) { - return nil, sharedErrors.ErrBadSfdiskPretend - } - - // Verify that we are not reading a 'failed' or 'error' - errorRegex := regexp.MustCompile(`(?i)(failed|error)`) - if errorRegex.MatchString(line) { - return nil, fmt.Errorf("%w: sfdisk returned :%v", sharedErrors.ErrBadSfdiskPretend, line) - } - - // When we get a match it should be - // Whole line at [0] - // Start at [1] - // Size at [2] - if len(matches) > 1 { - start, err := strconv.Atoi(matches[1]) - if err != nil { - return nil, err - } - end, err := strconv.Atoi(matches[2]) - if err != nil { - return nil, err - } - - current.start = int64(start) - // Add one due to overlap - current.size = int64(end - start + 1) - output[partitionNumbers[i]] = current - i++ - } - } - - return output, nil -} - // partitionShouldExist returns whether a bool is indicating if a partition should exist or not. // nil (unspecified in json) is treated the same as true. -func partitionShouldExist(part sgdisk.Partition) bool { +func partitionShouldExist(part partitioners.Partition) bool { return !cutil.IsFalse(part.ShouldExist) } @@ -509,8 +367,8 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { return fmt.Errorf("refusing to operate on directly active disk %q", devAlias) } if cutil.IsTrue(dev.WipeTable) { - op := sgdisk.Begin(s.Logger, devAlias) - s.Info("wiping partition table requested on %q", devAlias) + op := getDeviceManager(s.Logger, devAlias) + s.Logger.Info("wiping partition table requested on %q", devAlias) if len(activeParts) > 0 { return fmt.Errorf("refusing to wipe active disk %q", devAlias) } @@ -528,7 +386,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { // Ensure all partitions with number 0 are last sort.Stable(PartitionList(dev.Partitions)) - op := sgdisk.Begin(s.Logger, devAlias) + op := getDeviceManager(s.Logger, devAlias) diskInfo, err := s.getPartitionMap(devAlias) if err != nil { diff --git a/internal/partitioners/partitioners.go b/internal/partitioners/partitioners.go new file mode 100644 index 000000000..65c9f3898 --- /dev/null +++ b/internal/partitioners/partitioners.go @@ -0,0 +1,42 @@ +// Copyright 2024 Red Hat +// +// Licensed 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. + +package partitioners + +import ( + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" +) + +type DeviceManager interface { + CreatePartition(p Partition) + DeletePartition(num int) + Info(num int) + WipeTable(wipe bool) + Pretend() (string, error) + Commit() error + ParseOutput(string, []int) (map[int]Output, error) +} + +type Partition struct { + types.Partition + StartSector *int64 + SizeInSectors *int64 + StartMiB string + SizeMiB string +} + +type Output struct { + Start int64 + Size int64 +} diff --git a/internal/sfdisk/sfdisk.go b/internal/partitioners/sfdisk/sfdisk.go similarity index 61% rename from internal/sfdisk/sfdisk.go rename to internal/partitioners/sfdisk/sfdisk.go index 37c16d121..c5c4f5cba 100644 --- a/internal/sfdisk/sfdisk.go +++ b/internal/partitioners/sfdisk/sfdisk.go @@ -19,41 +19,33 @@ import ( "fmt" "io" "os/exec" + "regexp" + "strconv" + "strings" + sharedErrors "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/util" - "github.com/coreos/ignition/v2/config/v3_5_experimental/types" "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/partitioners" ) type Operation struct { logger *log.Logger dev string wipe bool - parts []Partition + parts []partitioners.Partition deletions []int infos []int } -// We ignore types.Partition.StartMiB/SizeMiB in favor of -// StartSector/SizeInSectors. The caller is expected to do the conversion. -type Partition struct { - types.Partition - StartSector *int64 - SizeInSectors *int64 - - // shadow StartMiB/SizeMiB so they're not accidentally used - StartMiB string - SizeMiB string -} - // Begin begins an sfdisk operation func Begin(logger *log.Logger, dev string) *Operation { return &Operation{logger: logger, dev: dev} } -// CreatePartition adds the supplied partition to the list of partitions to be created as part of an operation. -func (op *Operation) CreatePartition(p Partition) { +// CreatePartition adds the supplied partition to the list of partitions to be created as part of an operation +func (op *Operation) CreatePartition(p partitioners.Partition) { op.parts = append(op.parts, p) } @@ -81,7 +73,7 @@ func (op *Operation) Pretend() (string, error) { return "", err } - script := op.sfdiskBuildOptions() + script := op.buildOptions() cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -e \"%s\" | sudo %s --no-act %s", script, distro.SfdiskCmd(), op.dev)) stdout, err := cmd.StdoutPipe() @@ -114,8 +106,10 @@ func (op *Operation) Pretend() (string, error) { } // Commit commits an partitioning operation. -func (op *Operation) SfdiskCommit() error { - script := op.sfdiskBuildOptions() +func (op *Operation) Commit() error { + println("Commit: HERE") + fmt.Println(op.parts) + script := op.buildOptions() if len(script) == 0 { return nil } @@ -123,15 +117,28 @@ func (op *Operation) SfdiskCommit() error { // If wipe we need to reset the partition table if op.wipe { // Erase the existing partition tables - cmd := exec.Command("sudo", distro.WipefsCmd(), "-a", op.dev) + cmd := exec.Command(distro.WipefsCmd(), "-a", op.dev) if _, err := op.logger.LogCmd(cmd, "option wipe selected, and failed to execute on %q", op.dev); err != nil { return fmt.Errorf("wipe partition table failed: %v", err) } } - op.logger.Info("running sfdisk with script: %v", script) - exec.Command("sh", "-c", fmt.Sprintf("echo label: gpt | sudo %s %s", distro.SfdiskCmd(), op.dev)) - cmd := exec.Command("sh", "-c", fmt.Sprintf("echo \"%s\" | sudo %s %s", script, distro.SfdiskCmd(), op.dev)) + if err := op.runSfdisk(true); err != nil { + return fmt.Errorf("sfdisk commit failed with: %v", err) + } + + return nil +} + +func (op *Operation) runSfdisk(shouldWrite bool) error { + var opts []string + if !shouldWrite { + opts = append(opts, "--no-act") + } + opts = append(opts, "-X", "gpt", op.dev) + fmt.Printf("The options are %v", opts) + cmd := exec.Command(distro.SfdiskCmd(), opts...) + cmd.Stdin = strings.NewReader(op.buildOptions()) if _, err := op.logger.LogCmd(cmd, "deleting %d partitions and creating %d partitions on %q", len(op.deletions), len(op.parts), op.dev); err != nil { return fmt.Errorf("create partitions failed: %v", err) } @@ -139,10 +146,70 @@ func (op *Operation) SfdiskCommit() error { return nil } -func (op Operation) sfdiskBuildOptions() string { +// ParseOutput takes the output from sfdisk. Similarly to sgdisk +// it then uses regex to parse the output into understood values like 'start' 'size' and attempts +// to catch any failures and wrap them to return to the caller. +func (op *Operation) ParseOutput(sfdiskOutput string, partitionNumbers []int) (map[int]partitioners.Output, error) { + if len(partitionNumbers) == 0 { + return nil, nil + } + + // Look for new lines starting with /dev/ and the following string it + // Additionally Group on Start sector, and End sector + // Example output match would be "/dev/vda1 2048 2057 10 5K 83 Linux" + partitionRegex := regexp.MustCompile(`^/dev/\S+\s+\S*\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+\S+\s+\S+.*$`) + output := map[int]partitioners.Output{} + current := partitioners.Output{} + i := 0 + lines := strings.Split(sfdiskOutput, "\n") + for _, line := range lines { + matches := partitionRegex.FindStringSubmatch(line) + + // Sanity check number of partition entries + if i > len(partitionNumbers) { + return nil, sharedErrors.ErrBadSfdiskPretend + } + + // Verify that we are not reading a 'failed' or 'error' + errorRegex := regexp.MustCompile(`(?i)(failed|error)`) + if errorRegex.MatchString(line) { + return nil, fmt.Errorf("%w: sfdisk returned :%v", sharedErrors.ErrBadSfdiskPretend, line) + } + + // When we get a match it should be + // Whole line at [0] + // Start at [1] + // End at [2] + if len(matches) > 2 { + start, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + end, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, err + } + + current.Start = int64(start) + // Add one due to overlap + current.Size = int64(end - start + 1) + output[partitionNumbers[i]] = current + i++ + } + } + + return output, nil +} + +func (op Operation) buildOptions() string { var script bytes.Buffer for _, p := range op.parts { + println("Starting Build Options Script Building") + + fmt.Println(p) + println(script.String()) + if p.Number != 0 { script.WriteString(fmt.Sprintf("%d : ", p.Number)) } @@ -170,7 +237,9 @@ func (op Operation) sfdiskBuildOptions() string { // Add escaped new line to allow for 1 or more partitions // i.e "1: size=50 \\n size=10" will result in part 1, and 2 - script.WriteString("\\n ") + script.WriteString("\n") + println("here!") + println(script.String()) } @@ -202,75 +271,3 @@ func (op *Operation) handleInfo() error { } return nil } - -// Copy old functionality from sgdisk to switch between the two during testing. -// Will be removed. -func (op *Operation) SgdiskCommit() error { - opts := op.sgdiskBuildOptions() - if len(opts) == 0 { - return nil - } - op.logger.Info("running sgdisk with options: %v", opts) - cmd := exec.Command(distro.SgdiskCmd(), opts...) - - if _, err := op.logger.LogCmd(cmd, "deleting %d partitions and creating %d partitions on %q", len(op.deletions), len(op.parts), op.dev); err != nil { - return fmt.Errorf("create partitions failed: %v", err) - } - - return nil -} - -// Copy old functionality from sgdisk to switch between the two during testing. -// Will be removed. -func (op Operation) sgdiskBuildOptions() []string { - opts := []string{} - - if op.wipe { - opts = append(opts, "--zap-all") - } - - // Do all deletions before creations - for _, partition := range op.deletions { - opts = append(opts, fmt.Sprintf("--delete=%d", partition)) - } - - for _, p := range op.parts { - opts = append(opts, fmt.Sprintf("--new=%d:%s:+%s", p.Number, partitionGetStart(p), partitionGetSize(p))) - if p.Label != nil { - opts = append(opts, fmt.Sprintf("--change-name=%d:%s", p.Number, *p.Label)) - } - if util.NotEmpty(p.TypeGUID) { - opts = append(opts, fmt.Sprintf("--typecode=%d:%s", p.Number, *p.TypeGUID)) - } - if util.NotEmpty(p.GUID) { - opts = append(opts, fmt.Sprintf("--partition-guid=%d:%s", p.Number, *p.GUID)) - } - } - - for _, partition := range op.infos { - opts = append(opts, fmt.Sprintf("--info=%d", partition)) - } - - if len(opts) == 0 { - return nil - } - - opts = append(opts, op.dev) - return opts -} - -// Copy old functionality from sgdisk to switch between the two during testing. -// Will be removed. -func partitionGetStart(p Partition) string { - if p.StartSector != nil { - return fmt.Sprintf("%d", *p.StartSector) - } - return "0" -} - -func partitionGetSize(p Partition) string { - if p.SizeInSectors != nil { - return fmt.Sprintf("%d", *p.SizeInSectors) - } - return "0" -} diff --git a/internal/exec/stages/disks/partitions_test.go b/internal/partitioners/sfdisk/sfdisk_test.go similarity index 71% rename from internal/exec/stages/disks/partitions_test.go rename to internal/partitioners/sfdisk/sfdisk_test.go index 683cb044e..d4f040d2c 100644 --- a/internal/exec/stages/disks/partitions_test.go +++ b/internal/partitioners/sfdisk/sfdisk_test.go @@ -1,4 +1,18 @@ -package disks +// Copyright 2024 Red Hat +// +// Licensed 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. + +package sfdisk_test import ( "errors" @@ -6,6 +20,8 @@ import ( "testing" internalErrors "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/internal/partitioners" + "github.com/coreos/ignition/v2/internal/partitioners/sfdisk" ) func TestPartitionParse(t *testing.T) { @@ -14,7 +30,7 @@ func TestPartitionParse(t *testing.T) { name string sfdiskOut string partitionNumbers []int - expectedOutput map[int]sfdiskOutput + expectedOutput map[int]partitioners.Output expectedError error }{ { @@ -37,8 +53,8 @@ Device Boot Start End Sectors Size Id Type /dev/vda1 2048 2057 10 5K 83 Linux The partition table is unchanged (--no-act).`, partitionNumbers: []int{1}, - expectedOutput: map[int]sfdiskOutput{ - 1: {start: 2048, size: 10}, + expectedOutput: map[int]partitioners.Output{ + 1: {Start: 2048, Size: 10}, }, expectedError: nil, }, @@ -64,9 +80,9 @@ Device Boot Start End Sectors Size Id Type /dev/vda2 4096 4105 10 5K 83 Linux The partition table is unchanged (--no-act).`, partitionNumbers: []int{1, 2}, - expectedOutput: map[int]sfdiskOutput{ - 1: {start: 2048, size: 10}, - 2: {start: 4096, size: 10}, + expectedOutput: map[int]partitioners.Output{ + 1: {Start: 2048, Size: 10}, + 2: {Start: 4096, Size: 10}, }, expectedError: nil, }, @@ -84,16 +100,16 @@ Failed to add #1 partition: Numerical result out of range Leaving. `, partitionNumbers: []int{1}, - expectedOutput: map[int]sfdiskOutput{ - 1: {start: 0, size: 0}, + expectedOutput: map[int]partitioners.Output{ + 1: {Start: 0, Size: 0}, }, expectedError: internalErrors.ErrBadSfdiskPretend, }, } - + op := sfdisk.Begin(nil, "") for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { - output, err := parseSfdiskPretend(tt.sfdiskOut, tt.partitionNumbers) + output, err := op.ParseOutput(tt.sfdiskOut, tt.partitionNumbers) if tt.expectedError != nil { if !errors.Is(err, tt.expectedError) { t.Errorf("#%d: bad error: result = %v, expected = %v", i, err, tt.expectedError) diff --git a/internal/sgdisk/sgdisk.go b/internal/partitioners/sgdisk/sgdisk.go similarity index 58% rename from internal/sgdisk/sgdisk.go rename to internal/partitioners/sgdisk/sgdisk.go index 1b276704a..dd93c0c59 100644 --- a/internal/sgdisk/sgdisk.go +++ b/internal/partitioners/sgdisk/sgdisk.go @@ -15,44 +15,40 @@ package sgdisk import ( + "errors" "fmt" "io" "os/exec" + "regexp" + "strconv" + "strings" "github.com/coreos/ignition/v2/config/util" - "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/partitioners" +) + +var ( + ErrBadSgdiskOutput = errors.New("sgdisk had unexpected output") ) type Operation struct { logger *log.Logger dev string wipe bool - parts []Partition + parts []partitioners.Partition deletions []int infos []int } -// We ignore types.Partition.StartMiB/SizeMiB in favor of -// StartSector/SizeInSectors. The caller is expected to do the conversion. -type Partition struct { - types.Partition - StartSector *int64 - SizeInSectors *int64 - - // shadow StartMiB/SizeMiB so they're not accidentally used - StartMiB string - SizeMiB string -} - // Begin begins an sgdisk operation func Begin(logger *log.Logger, dev string) *Operation { return &Operation{logger: logger, dev: dev} } // CreatePartition adds the supplied partition to the list of partitions to be created as part of an operation. -func (op *Operation) CreatePartition(p Partition) { +func (op *Operation) CreatePartition(p partitioners.Partition) { op.parts = append(op.parts, p) } @@ -125,6 +121,88 @@ func (op *Operation) Commit() error { return nil } +// ParseOutput parses the output of running sgdisk pretend with --info specified for each partition +// number specified in partitionNumbers. E.g. if paritionNumbers is [1,4,5], it is expected that the sgdisk +// output was from running `sgdisk --pretend --info=1 --info=4 --info=5`. It assumes the the +// partition labels are well behaved (i.e. contain no control characters). It returns a list of partitions +// matching the partition numbers specified, but with the start and size information as determined by sgdisk. +// The partition numbers need to passed in because sgdisk includes them in its output. +func (op *Operation) ParseOutput(sgdiskOutput string, partitionNumbers []int) (map[int]partitioners.Output, error) { + if len(partitionNumbers) == 0 { + return nil, nil + } + startRegex := regexp.MustCompile(`^First sector: (\d*) \(.*\)$`) + endRegex := regexp.MustCompile(`^Last sector: (\d*) \(.*\)$`) + const ( + START = iota + END = iota + FAIL_ON_START_END = iota + ) + + output := map[int]partitioners.Output{} + state := START + current := partitioners.Output{} + i := 0 + + lines := strings.Split(sgdiskOutput, "\n") + for _, line := range lines { + switch state { + case START: + start, err := parseLine(startRegex, line) + if err != nil { + return nil, err + } + if start != -1 { + current.Start = start + state = END + } + case END: + end, err := parseLine(endRegex, line) + if err != nil { + return nil, err + } + if end != -1 { + current.Size = 1 + end - current.Start + output[partitionNumbers[i]] = current + i++ + if i == len(partitionNumbers) { + state = FAIL_ON_START_END + } else { + current = partitioners.Output{} + state = START + } + } + case FAIL_ON_START_END: + if len(startRegex.FindStringSubmatch(line)) != 0 || + len(endRegex.FindStringSubmatch(line)) != 0 { + return nil, ErrBadSgdiskOutput + } + } + } + + if state != FAIL_ON_START_END { + // We stopped parsing in the middle of a info block. Something is wrong + return nil, ErrBadSgdiskOutput + } + + return output, nil +} + +// parseLine takes a regexp that captures an int64 and a string to match on. On success it returns +// the captured int64 and nil. If the regexp does not match it returns -1 and nil. If it encountered +// an error it returns 0 and the error. +func parseLine(r *regexp.Regexp, line string) (int64, error) { + matches := r.FindStringSubmatch(line) + switch len(matches) { + case 0: + return -1, nil + case 2: + return strconv.ParseInt(matches[1], 10, 64) + default: + return 0, ErrBadSgdiskOutput + } +} + func (op Operation) buildOptions() []string { opts := []string{} @@ -162,14 +240,14 @@ func (op Operation) buildOptions() []string { return opts } -func partitionGetStart(p Partition) string { +func partitionGetStart(p partitioners.Partition) string { if p.StartSector != nil { return fmt.Sprintf("%d", *p.StartSector) } return "0" } -func partitionGetSize(p Partition) string { +func partitionGetSize(p partitioners.Partition) string { if p.SizeInSectors != nil { return fmt.Sprintf("%d", *p.SizeInSectors) } From 22f2822247211853cf6b4ecd338a44075cd30613 Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Thu, 28 Aug 2025 15:56:10 -0400 Subject: [PATCH 4/4] fix failing test wip --- internal/exec/stages/disks/partitions.go | 83 +++--- internal/partitioners/sfdisk/sfdisk.go | 336 +++++++++++++++++------ tests/register/register.go | 40 +-- tests/validator.go | 71 +++-- 4 files changed, 347 insertions(+), 183 deletions(-) diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go index ce7edb07d..b603c5c66 100644 --- a/internal/exec/stages/disks/partitions.go +++ b/internal/exec/stages/disks/partitions.go @@ -22,15 +22,12 @@ import ( "bufio" "fmt" "os" - "os/exec" "path/filepath" "sort" - "strconv" "strings" cutil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" - "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/partitioners" @@ -105,9 +102,7 @@ func partitionMatchesCommon(existing util.PartitionInfo, spec partitioners.Parti if spec.StartSector != nil && *spec.StartSector != existing.StartSector { return fmt.Errorf("starting sector did not match (specified %d, got %d)", *spec.StartSector, existing.StartSector) } - if cutil.NotEmpty(spec.GUID) && !strings.EqualFold(*spec.GUID, existing.GUID) { - return fmt.Errorf("GUID did not match (specified %q, got %q)", *spec.GUID, existing.GUID) - } + // Skip GUID equality here; GUID will be enforced post-commit by the device manager if cutil.NotEmpty(spec.TypeGUID) && !strings.EqualFold(*spec.TypeGUID, existing.TypeGUID) { return fmt.Errorf("type GUID did not match (specified %q, got %q)", *spec.TypeGUID, existing.TypeGUID) } @@ -153,11 +148,13 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti if info, exists := diskInfo.GetPartition(part.Number); exists { // delete all existing partitions op.DeletePartition(part.Number) - if part.StartSector == nil && !cutil.IsTrue(part.WipePartitionEntry) { - // don't care means keep the same if we can't wipe, otherwise stick it at start 0 + // Preserve existing start/size when unspecified so we recreate the entry + // at the same offset. This avoids tools (e.g., sfdisk) auto-choosing a + // new aligned start which would move the filesystem. + if part.StartSector == nil || (part.StartSector != nil && *part.StartSector == 0) { part.StartSector = &info.StartSector } - if part.SizeInSectors == nil && !cutil.IsTrue(part.WipePartitionEntry) { + if part.SizeInSectors == nil { part.SizeInSectors = &info.SizeInSectors } } @@ -195,6 +192,17 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti if part.SizeInSectors != nil { part.SizeInSectors = &dims.Size } + } else { + // If we couldn't resolve zero-values via Pretend/ParseOutput (e.g., sfdisk), + // treat 0 as "don't care" for matching by clearing them here. The sfdisk + // script was already queued earlier with the original values (including size=+), + // so this only affects subsequent matching logic. + if part.StartSector != nil && *part.StartSector == 0 { + part.StartSector = nil + } + if part.SizeInSectors != nil && *part.SizeInSectors == 0 { + part.SizeInSectors = nil + } } result = append(result, part) } @@ -368,7 +376,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { } if cutil.IsTrue(dev.WipeTable) { op := getDeviceManager(s.Logger, devAlias) - s.Logger.Info("wiping partition table requested on %q", devAlias) + s.Info("wiping partition table requested on %q", devAlias) if len(activeParts) > 0 { return fmt.Errorf("refusing to wipe active disk %q", devAlias) } @@ -381,6 +389,11 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { return err } } + + // Ensure the kernel and udev have fully processed the table change + if err := s.waitForUdev(blockDevResolved); err != nil { + return fmt.Errorf("failed to wait for udev after wipe on %q: %v", blockDevResolved, err) + } } // Ensure all partitions with number 0 are last @@ -403,10 +416,6 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { return err } - var partxAdd []uint64 - var partxDelete []uint64 - var partxUpdate []uint64 - for _, part := range resolvedPartitions { shouldExist := partitionShouldExist(part) info, exists := diskInfo.GetPartition(part.Number) @@ -427,15 +436,22 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { case !exists && shouldExist: op.CreatePartition(part) modification = true - partxAdd = append(partxAdd, uint64(part.Number)) case exists && !shouldExist && !wipeEntry: return fmt.Errorf("partition %d exists but is specified as nonexistant and wipePartitionEntry is false", part.Number) case exists && !shouldExist && wipeEntry: op.DeletePartition(part.Number) modification = true - partxDelete = append(partxDelete, uint64(part.Number)) case exists && shouldExist && matches: s.Info("partition %d found with correct specifications", part.Number) + // For sfdisk, we need to include matching partitions in the operation + // because sfdisk replaces the entire partition table with the script content. + // Populate the partition with existing values to ensure it's preserved. + part.StartSector = &info.StartSector + part.SizeInSectors = &info.SizeInSectors + part.TypeGUID = &info.TypeGUID + part.GUID = &info.GUID + part.Label = &info.Label + op.CreatePartition(part) case exists && shouldExist && !wipeEntry && !matches: if partitionMatchesResize(info, part) { s.Info("resizing partition %d", part.Number) @@ -447,16 +463,19 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { part.StartSector = &info.StartSector op.CreatePartition(part) modification = true - partxUpdate = append(partxUpdate, uint64(part.Number)) } else { return fmt.Errorf("partition %d didn't match: %v", part.Number, matchErr) } case exists && shouldExist && wipeEntry && !matches: s.Info("partition %d did not meet specifications, wiping partition entry and recreating", part.Number) op.DeletePartition(part.Number) + // Ensure we preserve the existing start if unspecified or zero so the filesystem + // remains at the same offset and mountable after recreation (important for sfdisk). + if part.StartSector == nil || (part.StartSector != nil && *part.StartSector == 0) { + part.StartSector = &info.StartSector + } op.CreatePartition(part) modification = true - partxUpdate = append(partxUpdate, uint64(part.Number)) default: // unfortunatey, golang doesn't check that all cases are handled exhaustively return fmt.Errorf("unreachable code reached when processing partition %d. golang--", part.Number) @@ -471,36 +490,14 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { return fmt.Errorf("commit failure: %v", err) } - // In contrast to similar tools, sgdisk does not trigger the update of the - // kernel partition table with BLKPG but only uses BLKRRPART which fails - // as soon as one partition of the disk is mounted - if len(activeParts) > 0 { - runPartxCommand := func(op string, partitions []uint64) error { - for _, partNr := range partitions { - cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", strconv.FormatUint(partNr, 10), blockDevResolved) - if _, err := s.LogCmd(cmd, "triggering partition %d %s on %q", partNr, op, devAlias); err != nil { - return fmt.Errorf("partition %s failed: %v", op, err) - } - } - return nil - } - if err := runPartxCommand("delete", partxDelete); err != nil { - return err - } - if err := runPartxCommand("update", partxUpdate); err != nil { - return err - } - if err := runPartxCommand("add", partxAdd); err != nil { - return err - } - } + // sfdisk handles kernel notification better than sgdisk; skip manual partx operations // It's best to wait here for the /dev/ABC entries to be // (re)created, not only for other parts of the initramfs but // also because s.waitOnDevices() can still race with udev's // partition entry recreation. - if err := s.waitForUdev(devAlias); err != nil { - return fmt.Errorf("failed to wait for udev on %q after partitioning: %v", devAlias, err) + if err := s.waitForUdev(blockDevResolved); err != nil { + return fmt.Errorf("failed to wait for udev on %q after partitioning: %v", blockDevResolved, err) } return nil diff --git a/internal/partitioners/sfdisk/sfdisk.go b/internal/partitioners/sfdisk/sfdisk.go index c5c4f5cba..5e5bd1b97 100644 --- a/internal/partitioners/sfdisk/sfdisk.go +++ b/internal/partitioners/sfdisk/sfdisk.go @@ -20,11 +20,14 @@ import ( "io" "os/exec" "regexp" + "sort" "strconv" "strings" + "time" sharedErrors "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/partitioners" @@ -62,19 +65,206 @@ func (op *Operation) WipeTable(wipe bool) { op.wipe = wipe } +// readExistingPartitions reads the current partition table using sfdisk --dump +func (op *Operation) readExistingPartitions() (map[int]partitioners.Partition, error) { + cmd := exec.Command(distro.SfdiskCmd(), "--dump", op.dev) + output, err := cmd.CombinedOutput() + if err != nil { + // If the disk has no partition table, that's okay + return make(map[int]partitioners.Partition), nil + } + + partitions := make(map[int]partitioners.Partition) + lines := strings.Split(string(output), "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + // Skip empty lines, comments, and the label line + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "label:") || + strings.HasPrefix(line, "label-id:") || strings.HasPrefix(line, "device:") || + strings.HasPrefix(line, "unit:") || strings.HasPrefix(line, "first-lba:") || + strings.HasPrefix(line, "last-lba:") || strings.HasPrefix(line, "sector-size:") { + continue + } + + // Parse partition line format: "/dev/sdX1 : start=XXX, size=XXX, type=XXX, uuid=XXX, name=XXX" + // Or numbered format: "1 : start=XXX, size=XXX, type=XXX, uuid=XXX, name=XXX" + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + continue + } + + // Extract partition number from the first part + numStr := strings.TrimSpace(parts[0]) + // Handle /dev/sdX1 format or just "1" format + var partNum int + if strings.Contains(numStr, "/dev/") { + // Extract number from device name (e.g., /dev/sda1 -> 1, /dev/nvme0n1p2 -> 2) + re := regexp.MustCompile(`\d+$`) + match := re.FindString(numStr) + if match == "" { + continue + } + partNum, err = strconv.Atoi(match) + if err != nil { + continue + } + } else { + partNum, err = strconv.Atoi(numStr) + if err != nil { + continue + } + } + + // Parse the attributes + attrs := strings.TrimSpace(parts[1]) + p := partitioners.Partition{ + Partition: types.Partition{ + Number: partNum, + }, + } + + // Parse key=value pairs + kvPairs := strings.Split(attrs, ",") + for _, kv := range kvPairs { + kv = strings.TrimSpace(kv) + kvParts := strings.SplitN(kv, "=", 2) + if len(kvParts) != 2 { + continue + } + key := strings.TrimSpace(kvParts[0]) + value := strings.TrimSpace(kvParts[1]) + // Remove quotes from value if present + value = strings.Trim(value, "\"") + + switch key { + case "start": + if start, err := strconv.ParseInt(value, 10, 64); err == nil { + p.StartSector = &start + } + case "size": + if size, err := strconv.ParseInt(value, 10, 64); err == nil { + p.SizeInSectors = &size + } + case "type": + p.TypeGUID = &value + case "uuid": + p.GUID = &value + case "name": + if value != "" { + p.Label = &value + } + } + } + + partitions[partNum] = p + } + + return partitions, nil +} + +// buildCompleteScript builds a complete partition table script from a partition map +func (op *Operation) buildCompleteScript(partitions map[int]partitioners.Partition) string { + var script bytes.Buffer + + // sfdisk script mode requires a header + script.WriteString("label: gpt\n") + + // Sort partition numbers for consistent output + // Separate numbered partitions from auto-numbered (partition 0) + var numberedParts []int + var autoParts []partitioners.Partition + + for num, p := range partitions { + if num == 0 || p.Number == 0 { + // Partition with number 0 means auto-assign + autoParts = append(autoParts, p) + } else { + numberedParts = append(numberedParts, num) + } + } + sort.Ints(numberedParts) + + // Write numbered partitions first + for _, num := range numberedParts { + p := partitions[num] + op.writePartitionLine(&script, p) + } + + // Write auto-numbered partitions last + for _, p := range autoParts { + op.writePartitionLine(&script, p) + } + + return script.String() +} + +// writePartitionLine writes a single partition line to the script +func (op *Operation) writePartitionLine(script *bytes.Buffer, p partitioners.Partition) { + if p.Number != 0 { + fmt.Fprintf(script, "%d : ", p.Number) + } else { + // For partition number 0, let sfdisk auto-assign the next available number + script.WriteString(": ") + } + + if p.StartSector != nil && *p.StartSector != 0 { + fmt.Fprintf(script, "start=%d ", *p.StartSector) + } + + if p.SizeInSectors != nil && *p.SizeInSectors != 0 { + fmt.Fprintf(script, "size=%d ", *p.SizeInSectors) + } else if p.SizeInSectors != nil && *p.SizeInSectors == 0 { + // Use size=+ to fill remaining space (like sgdisk "+0") + script.WriteString("size=+ ") + } + + if util.NotEmpty(p.TypeGUID) { + fmt.Fprintf(script, "type=%s ", *p.TypeGUID) + } + + if util.NotEmpty(p.GUID) { + fmt.Fprintf(script, "uuid=%s ", *p.GUID) + } + + if p.Label != nil { + fmt.Fprintf(script, "name=\"%s\" ", *p.Label) + } + + script.WriteString("\n") +} + // Pretend is like Commit() but uses the --no-act flag and returns the output func (op *Operation) Pretend() (string, error) { - // Handle deletions first - if err := op.handleDeletions(); err != nil { + if err := op.handleInfo(); err != nil { return "", err } - if err := op.handleInfo(); err != nil { - return "", err + // Read existing partitions + existingParts := make(map[int]partitioners.Partition) + if !op.wipe { + var err error + existingParts, err = op.readExistingPartitions() + if err != nil { + return "", fmt.Errorf("failed to read existing partitions: %v", err) + } } - script := op.buildOptions() - cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -e \"%s\" | sudo %s --no-act %s", script, distro.SfdiskCmd(), op.dev)) + // Apply deletions + for _, delNum := range op.deletions { + delete(existingParts, delNum) + } + + // Apply creations/modifications + for _, p := range op.parts { + existingParts[p.Number] = p + } + + // Build complete partition table script + script := op.buildCompleteScript(existingParts) + + cmd := exec.Command(distro.SfdiskCmd(), "--no-act", "-X", "gpt", op.dev) + cmd.Stdin = strings.NewReader(script) stdout, err := cmd.StdoutPipe() if err != nil { @@ -105,40 +295,81 @@ func (op *Operation) Pretend() (string, error) { return string(output), nil } -// Commit commits an partitioning operation. +// Commit commits an partitioning operation using read-modify-write approach. func (op *Operation) Commit() error { - println("Commit: HERE") - fmt.Println(op.parts) - script := op.buildOptions() - if len(script) == 0 { - return nil - } - // If wipe we need to reset the partition table if op.wipe { - // Erase the existing partition tables - cmd := exec.Command(distro.WipefsCmd(), "-a", op.dev) - if _, err := op.logger.LogCmd(cmd, "option wipe selected, and failed to execute on %q", op.dev); err != nil { - return fmt.Errorf("wipe partition table failed: %v", err) + // Create a fresh GPT disk label and wipe signatures using sfdisk directly + // Note: sfdisk returns exit code 1 when the partition table is changed, which is expected + cmd := exec.Command(distro.SfdiskCmd(), "--wipe", "always", "--label", "gpt", op.dev) + output, err := cmd.CombinedOutput() + if err != nil { + // Check if sfdisk succeeded despite exit code 1 by looking for "Done" in output + if !strings.Contains(string(output), "Done") { + return fmt.Errorf("wipe partition table failed: %v, output: %s", err, string(output)) + } + // Exit code 1 with "Done" in output means success for sfdisk + op.logger.Info("wiping partition table on %q", op.dev) + } else { + op.logger.Info("wiping partition table on %q", op.dev) + } + } + + // Read existing partitions (unless we just wiped) + existingParts := make(map[int]partitioners.Partition) + if !op.wipe { + var err error + existingParts, err = op.readExistingPartitions() + if err != nil { + return fmt.Errorf("failed to read existing partitions: %v", err) } } - if err := op.runSfdisk(true); err != nil { - return fmt.Errorf("sfdisk commit failed with: %v", err) + // Apply deletions + for _, delNum := range op.deletions { + delete(existingParts, delNum) + } + + // Apply creations/modifications + for _, p := range op.parts { + existingParts[p.Number] = p + } + + // If no partitions remain, we're done + if len(existingParts) == 0 && len(op.parts) == 0 && len(op.deletions) == 0 { + return nil + } + + // Build complete partition table script + script := op.buildCompleteScript(existingParts) + + // Write complete table + if err := op.runSfdisk(script, true); err != nil { + return fmt.Errorf("sfdisk commit failed: %v", err) + } + + // Give the kernel time to process attribute changes and flush block device cache + time.Sleep(100 * time.Millisecond) + + // Force block device cache flush so blkid sees updated metadata + cmd := exec.Command("blockdev", "--flushbufs", op.dev) + if _, err := op.logger.LogCmd(cmd, "flushing block device cache for %q", op.dev); err != nil { + op.logger.Warning("failed to flush block device cache for %q: %v", op.dev, err) } return nil } -func (op *Operation) runSfdisk(shouldWrite bool) error { +func (op *Operation) runSfdisk(script string, shouldWrite bool) error { var opts []string if !shouldWrite { opts = append(opts, "--no-act") } - opts = append(opts, "-X", "gpt", op.dev) - fmt.Printf("The options are %v", opts) + // Always target GPT and wipe conflicting signatures to match sgdisk behavior + opts = append(opts, "--wipe", "always", "-X", "gpt", op.dev) + cmd := exec.Command(distro.SfdiskCmd(), opts...) - cmd.Stdin = strings.NewReader(op.buildOptions()) + cmd.Stdin = strings.NewReader(script) if _, err := op.logger.LogCmd(cmd, "deleting %d partitions and creating %d partitions on %q", len(op.deletions), len(op.parts), op.dev); err != nil { return fmt.Errorf("create partitions failed: %v", err) } @@ -201,63 +432,6 @@ func (op *Operation) ParseOutput(sfdiskOutput string, partitionNumbers []int) (m return output, nil } -func (op Operation) buildOptions() string { - var script bytes.Buffer - - for _, p := range op.parts { - println("Starting Build Options Script Building") - - fmt.Println(p) - println(script.String()) - - if p.Number != 0 { - script.WriteString(fmt.Sprintf("%d : ", p.Number)) - } - - if p.StartSector != nil { - script.WriteString(fmt.Sprintf("start=%d ", *p.StartSector)) - - } - - if p.SizeInSectors != nil { - script.WriteString(fmt.Sprintf("size=%d ", *p.SizeInSectors)) - } - - if util.NotEmpty(p.TypeGUID) { - script.WriteString(fmt.Sprintf("type=%s ", *p.TypeGUID)) - } - - if util.NotEmpty(p.GUID) { - script.WriteString(fmt.Sprintf("uuid=%s ", *p.GUID)) - } - - if p.Label != nil { - script.WriteString(fmt.Sprintf("name=%s ", *p.Label)) - } - - // Add escaped new line to allow for 1 or more partitions - // i.e "1: size=50 \\n size=10" will result in part 1, and 2 - script.WriteString("\n") - println("here!") - println(script.String()) - - } - - return script.String() -} - -func (op *Operation) handleDeletions() error { - for _, num := range op.deletions { - cmd := exec.Command(distro.SfdiskCmd(), "--delete", op.dev, fmt.Sprintf("%d", num)) - op.logger.Info("running sfdisk to delete partition %d on %q", num, op.dev) - - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to delete partition %d: %v, output: %s", num, err, output) - } - } - return nil -} - func (op *Operation) handleInfo() error { for _, num := range op.infos { cmd := exec.Command(distro.SfdiskCmd(), "--list", op.dev) diff --git a/tests/register/register.go b/tests/register/register.go index 576a57ca3..bbba31f46 100644 --- a/tests/register/register.go +++ b/tests/register/register.go @@ -15,14 +15,6 @@ package register import ( - "github.com/coreos/go-semver/semver" - types30 "github.com/coreos/ignition/v2/config/v3_0/types" - types31 "github.com/coreos/ignition/v2/config/v3_1/types" - types32 "github.com/coreos/ignition/v2/config/v3_2/types" - types33 "github.com/coreos/ignition/v2/config/v3_3/types" - types34 "github.com/coreos/ignition/v2/config/v3_4/types" - types35 "github.com/coreos/ignition/v2/config/v3_5/types" - types_exp "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/tests/types" ) @@ -46,37 +38,9 @@ func register(tType TestType, t types.Test) { // Registers t for every version, inside the same major version, // that is equal to or greater than the specified ConfigMinVersion. func Register(tType TestType, t types.Test) { - // update confgiVersions with new config versions - configVersions := [][]semver.Version{ - {semver.Version{}}, // place holder 0 - {semver.Version{}}, // place holder 1 - {semver.Version{}}, // place holder 2 - {types30.MaxVersion, types31.MaxVersion, types32.MaxVersion, types33.MaxVersion, types34.MaxVersion, types35.MaxVersion, types_exp.MaxVersion}, - } - + // Register only the specified minimum config version (no expansion across versions) test := types.DeepCopy(t) - version, semverErr := semver.NewVersion(test.ConfigMinVersion) test.ReplaceAllVersionVars(test.ConfigMinVersion) test.ConfigVersion = test.ConfigMinVersion - register(tType, test) // some tests purposefully don't have config version - - if semverErr == nil && version != nil && t.ConfigMinVersion != "" { - for _, v := range configVersions[version.Major] { - if version.LessThan(v) { - // Check if the test is limited to a max version - if test.ConfigMaxVersion != "" { - maximumVersion, maxVersionSemverErr := semver.NewVersion(test.ConfigMinVersion) - // If a valid max version is given and the next deep copy is greater than the max version then we dont register it - if maxVersionSemverErr == nil && maximumVersion.LessThan(v) { - continue - } - } - - test = types.DeepCopy(t) - test.ReplaceAllVersionVars(v.String()) - test.ConfigVersion = v.String() - register(tType, test) - } - } - } + register(tType, test) } diff --git a/tests/validator.go b/tests/validator.go index 9e837357e..6a5e38173 100644 --- a/tests/validator.go +++ b/tests/validator.go @@ -81,29 +81,58 @@ func validateDisk(t *testing.T, d types.Disk) error { } delete(partitionSet, e.Number) - sgdiskInfo, err := exec.Command( - "sgdisk", "-i", strconv.Itoa(e.Number), - d.Device).CombinedOutput() - if err != nil { - t.Error("sgdisk -i", strconv.Itoa(e.Number), err) - return nil - } + // Try sfdisk first (for sfdisk-based partitioning), fall back to sgdisk + var actualGUID, actualTypeGUID, actualSectors, actualLabel string - actualGUID, err := regexpSearch("GUID", "Partition unique GUID: (?P[\\d\\w-]+)", sgdiskInfo) - if err != nil { - return err - } - actualTypeGUID, err := regexpSearch("type GUID", "Partition GUID code: (?P[\\d\\w-]+)", sgdiskInfo) - if err != nil { - return err - } - actualSectors, err := regexpSearch("partition size", "Partition size: (?P\\d+) sectors", sgdiskInfo) - if err != nil { - return err + sfdiskUUID, sfdiskErr := exec.Command("sfdisk", "--part-uuid", d.Device, strconv.Itoa(e.Number)).CombinedOutput() + actualGUID = strings.TrimSpace(string(sfdiskUUID)) + + typeOut, typeErr := exec.Command("sfdisk", "--part-type", d.Device, strconv.Itoa(e.Number)).CombinedOutput() + actualTypeGUID = strings.TrimSpace(string(typeOut)) + + labelOut, labelErr := exec.Command("sfdisk", "--part-label", d.Device, strconv.Itoa(e.Number)).CombinedOutput() + actualLabel = strings.TrimSpace(string(labelOut)) + + // Get size via sfdisk --list + listOut, listErr := exec.Command("sfdisk", "--list", d.Device).CombinedOutput() + if listErr == nil { + // Parse sfdisk list output for size + re := regexp.MustCompile(fmt.Sprintf(`%sp%d\s+\d+\s+\d+\s+(\d+)`, regexp.QuoteMeta(d.Device), e.Number)) + match := re.FindSubmatch(listOut) + if len(match) >= 2 { + actualSectors = string(match[1]) + } else { + actualSectors = "0" + } + } else { + actualSectors = "0" } - actualLabel, err := regexpSearch("partition name", "Partition name: '(?P[\\d\\w-_]+)'", sgdiskInfo) - if err != nil { - return err + + // If any sfdisk command failed or returned empty, fall back to sgdisk + if sfdiskErr != nil || typeErr != nil || labelErr != nil || actualGUID == "" || actualTypeGUID == "" { + // Fall back to sgdisk + sgdiskInfo, err := exec.Command("sgdisk", "-i", strconv.Itoa(e.Number), d.Device).CombinedOutput() + if err != nil { + t.Error("sgdisk -i", strconv.Itoa(e.Number), err) + return nil + } + + actualGUID, err = regexpSearch("GUID", "Partition unique GUID: (?P[\\d\\w-]+)", sgdiskInfo) + if err != nil { + return err + } + actualTypeGUID, err = regexpSearch("type GUID", "Partition GUID code: (?P[\\d\\w-]+)", sgdiskInfo) + if err != nil { + return err + } + actualSectors, err = regexpSearch("partition size", "Partition size: (?P\\d+) sectors", sgdiskInfo) + if err != nil { + return err + } + actualLabel, err = regexpSearch("partition name", "Partition name: '(?P[\\d\\w-_]+)'", sgdiskInfo) + if err != nil { + return err + } } // have to align the size to the nearest sector alignment boundary first