Skip to content

Commit 32b61d7

Browse files
committed
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
1 parent 075a844 commit 32b61d7

File tree

5 files changed

+283
-294
lines changed

5 files changed

+283
-294
lines changed

internal/exec/stages/disks/partitions.go

+26-168
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,32 @@ package disks
2020

2121
import (
2222
"bufio"
23-
"errors"
2423
"fmt"
2524
"os"
2625
"os/exec"
2726
"path/filepath"
28-
"regexp"
2927
"sort"
3028
"strconv"
3129
"strings"
3230

33-
sharedErrors "github.com/coreos/ignition/v2/config/shared/errors"
3431
cutil "github.com/coreos/ignition/v2/config/util"
3532
"github.com/coreos/ignition/v2/config/v3_5_experimental/types"
3633
"github.com/coreos/ignition/v2/internal/distro"
3734
"github.com/coreos/ignition/v2/internal/exec/util"
38-
"github.com/coreos/ignition/v2/internal/sgdisk"
35+
"github.com/coreos/ignition/v2/internal/log"
36+
"github.com/coreos/ignition/v2/internal/partitioners"
37+
"github.com/coreos/ignition/v2/internal/partitioners/sfdisk"
38+
"github.com/coreos/ignition/v2/internal/partitioners/sgdisk"
3939
iutil "github.com/coreos/ignition/v2/internal/util"
4040
)
4141

42-
var (
43-
ErrBadSgdiskOutput = errors.New("sgdisk had unexpected output")
44-
)
42+
func getDeviceManager(logger *log.Logger, dev string) partitioners.DeviceManager {
43+
// To be replaced with build tag support or something similar.
44+
if false {
45+
return sgdisk.Begin(logger, dev)
46+
}
47+
return sfdisk.Begin(logger, dev)
48+
}
4549

4650
// createPartitions creates the partitions described in config.Storage.Disks.
4751
func (s stage) createPartitions(config types.Config) error {
@@ -76,7 +80,7 @@ func (s stage) createPartitions(config types.Config) error {
7680

7781
// partitionMatches determines if the existing partition matches the spec given. See doc/operator notes for what
7882
// what it means for an existing partition to match the spec. spec must have non-zero Start and Size.
79-
func partitionMatches(existing util.PartitionInfo, spec sgdisk.Partition) error {
83+
func partitionMatches(existing util.PartitionInfo, spec partitioners.Partition) error {
8084
if err := partitionMatchesCommon(existing, spec); err != nil {
8185
return err
8286
}
@@ -88,13 +92,13 @@ func partitionMatches(existing util.PartitionInfo, spec sgdisk.Partition) error
8892

8993
// partitionMatchesResize returns if the existing partition should be resized by evaluating if
9094
// `resize`field is true and partition matches in all respects except size.
91-
func partitionMatchesResize(existing util.PartitionInfo, spec sgdisk.Partition) bool {
95+
func partitionMatchesResize(existing util.PartitionInfo, spec partitioners.Partition) bool {
9296
return cutil.IsTrue(spec.Resize) && partitionMatchesCommon(existing, spec) == nil
9397
}
9498

9599
// partitionMatchesCommon handles the common tests (excluding the partition size) to determine
96100
// if the existing partition matches the spec given.
97-
func partitionMatchesCommon(existing util.PartitionInfo, spec sgdisk.Partition) error {
101+
func partitionMatchesCommon(existing util.PartitionInfo, spec partitioners.Partition) error {
98102
if spec.Number != existing.Number {
99103
return fmt.Errorf("partition numbers did not match (specified %d, got %d). This should not happen, please file a bug.", spec.Number, existing.Number)
100104
}
@@ -114,7 +118,7 @@ func partitionMatchesCommon(existing util.PartitionInfo, spec sgdisk.Partition)
114118
}
115119

116120
// partitionShouldBeInspected returns if the partition has zeroes that need to be resolved to sectors.
117-
func partitionShouldBeInspected(part sgdisk.Partition) bool {
121+
func partitionShouldBeInspected(part partitioners.Partition) bool {
118122
if part.Number == 0 {
119123
return false
120124
}
@@ -134,17 +138,17 @@ func convertMiBToSectors(mib *int, sectorSize int) *int64 {
134138
// getRealStartAndSize returns a map of partition numbers to a struct that contains what their real start
135139
// and end sector should be. It runs sgdisk --pretend to determine what the partitions would look like if
136140
// everything specified were to be (re)created.
137-
func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]sgdisk.Partition, error) {
138-
partitions := []sgdisk.Partition{}
141+
func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]partitioners.Partition, error) {
142+
partitions := []partitioners.Partition{}
139143
for _, cpart := range dev.Partitions {
140-
partitions = append(partitions, sgdisk.Partition{
144+
partitions = append(partitions, partitioners.Partition{
141145
Partition: cpart,
142146
StartSector: convertMiBToSectors(cpart.StartMiB, diskInfo.LogicalSectorSize),
143147
SizeInSectors: convertMiBToSectors(cpart.SizeMiB, diskInfo.LogicalSectorSize),
144148
})
145149
}
146150

147-
op := sgdisk.Begin(s.Logger, devAlias)
151+
op := getDeviceManager(s.Logger, devAlias)
148152
for _, part := range partitions {
149153
if info, exists := diskInfo.GetPartition(part.Number); exists {
150154
// delete all existing partitions
@@ -177,175 +181,29 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti
177181
if err != nil {
178182
return nil, err
179183
}
180-
181-
realDimensions, err := parseSfdiskPretend(output, partitionsToInspect)
184+
realDimensions, err := op.ParseOutput(output, partitionsToInspect)
182185
if err != nil {
183186
return nil, err
184187
}
185188

186-
result := []sgdisk.Partition{}
189+
result := []partitioners.Partition{}
187190
for _, part := range partitions {
188191
if dims, ok := realDimensions[part.Number]; ok {
189192
if part.StartSector != nil {
190-
part.StartSector = &dims.start
193+
part.StartSector = &dims.Start
191194
}
192195
if part.SizeInSectors != nil {
193-
part.SizeInSectors = &dims.size
196+
part.SizeInSectors = &dims.Size
194197
}
195198
}
196199
result = append(result, part)
197200
}
198201
return result, nil
199202
}
200203

201-
type sgdiskOutput struct {
202-
start int64
203-
size int64
204-
}
205-
206-
// parseLine takes a regexp that captures an int64 and a string to match on. On success it returns
207-
// the captured int64 and nil. If the regexp does not match it returns -1 and nil. If it encountered
208-
// an error it returns 0 and the error.
209-
func parseLine(r *regexp.Regexp, line string) (int64, error) {
210-
matches := r.FindStringSubmatch(line)
211-
switch len(matches) {
212-
case 0:
213-
return -1, nil
214-
case 2:
215-
return strconv.ParseInt(matches[1], 10, 64)
216-
default:
217-
return 0, ErrBadSgdiskOutput
218-
}
219-
}
220-
221-
// parseSgdiskPretend parses the output of running sgdisk pretend with --info specified for each partition
222-
// number specified in partitionNumbers. E.g. if paritionNumbers is [1,4,5], it is expected that the sgdisk
223-
// output was from running `sgdisk --pretend <commands> --info=1 --info=4 --info=5`. It assumes the the
224-
// partition labels are well behaved (i.e. contain no control characters). It returns a list of partitions
225-
// matching the partition numbers specified, but with the start and size information as determined by sgdisk.
226-
// The partition numbers need to passed in because sgdisk includes them in its output.
227-
func parseSgdiskPretend(sgdiskOut string, partitionNumbers []int) (map[int]sgdiskOutput, error) {
228-
if len(partitionNumbers) == 0 {
229-
return nil, nil
230-
}
231-
startRegex := regexp.MustCompile(`^First sector: (\d*) \(.*\)$`)
232-
endRegex := regexp.MustCompile(`^Last sector: (\d*) \(.*\)$`)
233-
const (
234-
START = iota
235-
END = iota
236-
FAIL_ON_START_END = iota
237-
)
238-
239-
output := map[int]sgdiskOutput{}
240-
state := START
241-
current := sgdiskOutput{}
242-
i := 0
243-
244-
lines := strings.Split(sgdiskOut, "\n")
245-
for _, line := range lines {
246-
switch state {
247-
case START:
248-
start, err := parseLine(startRegex, line)
249-
if err != nil {
250-
return nil, err
251-
}
252-
if start != -1 {
253-
current.start = start
254-
state = END
255-
}
256-
case END:
257-
end, err := parseLine(endRegex, line)
258-
if err != nil {
259-
return nil, err
260-
}
261-
if end != -1 {
262-
current.size = 1 + end - current.start
263-
output[partitionNumbers[i]] = current
264-
i++
265-
if i == len(partitionNumbers) {
266-
state = FAIL_ON_START_END
267-
} else {
268-
current = sgdiskOutput{}
269-
state = START
270-
}
271-
}
272-
case FAIL_ON_START_END:
273-
if len(startRegex.FindStringSubmatch(line)) != 0 ||
274-
len(endRegex.FindStringSubmatch(line)) != 0 {
275-
return nil, ErrBadSgdiskOutput
276-
}
277-
}
278-
}
279-
280-
if state != FAIL_ON_START_END {
281-
// We stopped parsing in the middle of a info block. Something is wrong
282-
return nil, ErrBadSgdiskOutput
283-
}
284-
285-
return output, nil
286-
}
287-
288-
type sfdiskOutput struct {
289-
start int64
290-
size int64
291-
}
292-
293-
// ParsePretend takes the output from sfdisk running with the argument --no-act. Similar to sgdisk
294-
// it then uses regex to parse the output into understood values like 'start' 'size' and attempts
295-
// to catch any failures and wrap them to return to the caller.
296-
func parseSfdiskPretend(sfdiskOut string, partitionNumbers []int) (map[int]sfdiskOutput, error) {
297-
if len(partitionNumbers) == 0 {
298-
return nil, nil
299-
}
300-
301-
// Prepare the data, and a regex for matching on partitions
302-
partitionRegex := regexp.MustCompile(`^/dev/\S+\s+\S*\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+\S+\s+\S+.*$`)
303-
output := map[int]sfdiskOutput{}
304-
current := sfdiskOutput{}
305-
i := 0
306-
lines := strings.Split(sfdiskOut, "\n")
307-
for _, line := range lines {
308-
matches := partitionRegex.FindStringSubmatch(line)
309-
310-
// Sanity check number of partition entries
311-
if i > len(partitionNumbers) {
312-
return nil, sharedErrors.ErrBadSfdiskPretend
313-
}
314-
315-
// Verify that we are not reading a 'failed' or 'error'
316-
errorRegex := regexp.MustCompile(`(?i)(failed|error)`)
317-
if errorRegex.MatchString(line) {
318-
return nil, fmt.Errorf("%w: sfdisk returned :%v", sharedErrors.ErrBadSfdiskPretend, line)
319-
}
320-
321-
// When we get a match it should be
322-
// Whole line at [0]
323-
// Start at [1]
324-
// Size at [2]
325-
if len(matches) > 1 {
326-
start, err := strconv.Atoi(matches[1])
327-
if err != nil {
328-
return nil, err
329-
}
330-
end, err := strconv.Atoi(matches[2])
331-
if err != nil {
332-
return nil, err
333-
}
334-
335-
current.start = int64(start)
336-
// Add one due to overlap
337-
current.size = int64(end - start + 1)
338-
output[partitionNumbers[i]] = current
339-
i++
340-
}
341-
}
342-
343-
return output, nil
344-
}
345-
346204
// partitionShouldExist returns whether a bool is indicating if a partition should exist or not.
347205
// nil (unspecified in json) is treated the same as true.
348-
func partitionShouldExist(part sgdisk.Partition) bool {
206+
func partitionShouldExist(part partitioners.Partition) bool {
349207
return !cutil.IsFalse(part.ShouldExist)
350208
}
351209

@@ -497,7 +355,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
497355
return fmt.Errorf("refusing to operate on directly active disk %q", devAlias)
498356
}
499357
if cutil.IsTrue(dev.WipeTable) {
500-
op := sgdisk.Begin(s.Logger, devAlias)
358+
op := getDeviceManager(s.Logger, devAlias)
501359
s.Logger.Info("wiping partition table requested on %q", devAlias)
502360
if len(activeParts) > 0 {
503361
return fmt.Errorf("refusing to wipe active disk %q", devAlias)
@@ -516,7 +374,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
516374
// Ensure all partitions with number 0 are last
517375
sort.Stable(PartitionList(dev.Partitions))
518376

519-
op := sgdisk.Begin(s.Logger, devAlias)
377+
op := getDeviceManager(s.Logger, devAlias)
520378

521379
diskInfo, err := s.getPartitionMap(devAlias)
522380
if err != nil {

internal/partitioners/partitioners.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2024 Red Hat
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package partitioners
16+
17+
import (
18+
"github.com/coreos/ignition/v2/config/v3_5_experimental/types"
19+
)
20+
21+
type DeviceManager interface {
22+
CreatePartition(p Partition)
23+
DeletePartition(num int)
24+
Info(num int)
25+
WipeTable(wipe bool)
26+
Pretend() (string, error)
27+
Commit() error
28+
ParseOutput(string, []int) (map[int]Output, error)
29+
}
30+
31+
type Partition struct {
32+
types.Partition
33+
StartSector *int64
34+
SizeInSectors *int64
35+
StartMiB string
36+
SizeMiB string
37+
}
38+
39+
type Output struct {
40+
Start int64
41+
Size int64
42+
}

0 commit comments

Comments
 (0)