-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: sfdisk implementation #1929
base: main
Are you sure you want to change the base?
Changes from all commits
30329b9
26eda28
1cac765
88e9194
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package device_managers | ||
|
||
import ( | ||
"github.com/coreos/ignition/v2/config/v3_5_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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
// 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" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
sharedErrors "github.com/coreos/ignition/v2/config/shared/errors" | ||
"github.com/coreos/ignition/v2/config/util" | ||
"github.com/coreos/ignition/v2/internal/device_managers" | ||
"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 []device_managers.Partition | ||
deletions []int | ||
infos []int | ||
} | ||
|
||
// 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 device_managers.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.buildOptions() | ||
cmd := exec.Command("sh", "-c", fmt.Sprintf("echo -e \"%s\" | sudo %s --no-act %s", script, distro.SfdiskCmd(), op.dev)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's avoid passing through the shell here. We should be able to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, shouldn't need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I was running into permissions issues for some reason; (I agree) but it was failing on permissions. Maybe because of the "sh" ? |
||
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) Commit() error { | ||
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("sudo", distro.WipefsCmd(), "-a", op.dev) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't need |
||
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comments here. Probably cleanest is to make a helper function that actually calls |
||
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 | ||
} | ||
|
||
// 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]device_managers.Output, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK yeah, this is pretty unfortunate. I was expecting This has a high chance of regression if the output of sfdisk changes. We'll indeed want to make sure we have good coverage for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah; 100%, Ok will do and tag it here once done. |
||
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+.*$`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This deserves some explanation. Also, I feel like we should ignore anything before the "New situation:" line. |
||
output := map[int]device_managers.Output{} | ||
current := device_managers.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] | ||
// Size 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 { | ||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package sfdisk_test | ||
|
||
import ( | ||
"errors" | ||
"reflect" | ||
"testing" | ||
|
||
internalErrors "github.com/coreos/ignition/v2/config/shared/errors" | ||
"github.com/coreos/ignition/v2/internal/device_managers" | ||
"github.com/coreos/ignition/v2/internal/device_managers/sfdisk" | ||
) | ||
|
||
func TestPartitionParse(t *testing.T) { | ||
// Define test cases | ||
tests := []struct { | ||
name string | ||
sfdiskOut string | ||
partitionNumbers []int | ||
expectedOutput map[int]device_managers.Output | ||
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]device_managers.Output{ | ||
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]device_managers.Output{ | ||
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]device_managers.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 := 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) | ||
} | ||
} else if !reflect.DeepEqual(output, tt.expectedOutput) { | ||
t.Errorf("#%d: result = %v, expected = %v", i, output, tt.expectedOutput) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
partitioners
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah thats a better name; will do!