blob: 594048ed7af677c9dcd4b4ad6b4d16569da72e81 [file] [log] [blame]
// Copyright 2020 Google LLC
//
// 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 partutil
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
)
// minPartitionSizeSectors is the minimum partition size on COS in sectors. This
// size was chosen to maintain 4K alignment of partition start sectors (8
// sectors = 4K bytes).
const minPartitionSizeSectors = uint64(8)
// When we read disk information by dumping the partition table, we get output like the following:
// sudo sfdisk --dump /dev/sdb
// label: gpt
// label-id: 8071096F-DA33-154D-A687-AE097B8252C5
// device: /dev/sdb
// unit: sectors
// first-lba: 2048
// last-lba: 20971486
// /dev/sdb1 : start= 4401152, size= 2097152, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=3B41256B-E064-544A-9101-D2647C0B3A38
// /dev/sdb2 : start= 206848, size= 4194304, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=60E55EA1-4EEA-9F44-A066-4720F0129089
// /dev/sdb3 : start= 6498304, size= 204800, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=9479C34A-49A6-9442-A56F-956396DFAC20
// PartContent contains the info of a partition
type PartContent struct {
Start uint64
Size uint64
}
// HandlePartitionTable takes a partition table and get the start and size of the target partition.
// If change==true, it will rebuild the partition table with data passed in by p *PartContent
// and return the new table
func HandlePartitionTable(table, partName string, change bool, f func(p *PartContent)) (string, error) {
foundPartition := false
lines := strings.Split(table, "\n")
for idx, line := range lines {
// a white space is needed to prevent cases like /dev/sda14 matches /dev/sda1
if !strings.HasPrefix(line, partName+" ") {
continue
}
foundPartition = true
content := strings.Split(line, ":")
partInfo := strings.Split(content[1], ",")
startSec := strings.Split(partInfo[0], "=")
sizeSec := strings.Split(partInfo[1], "=")
var p PartContent
var err error
p.Start, err = strconv.ParseUint(strings.TrimSpace(startSec[1]), 10, 64)
if err != nil {
return "", fmt.Errorf("cannot convert %q to int, "+
"partition info: %q, error msg: (%v)", strings.TrimSpace(startSec[1]), line, err)
}
p.Size, err = strconv.ParseUint(strings.TrimSpace(sizeSec[1]), 10, 64)
if err != nil {
return "", fmt.Errorf("cannot convert %q to int, "+
"partition info: %q, error msg: (%v)", strings.TrimSpace(startSec[1]), line, err)
}
// run reading or changing function on the PartContent struct.
f(&p)
// need to rebuild the partition table.
if change {
startSec[1] = strconv.FormatUint(p.Start, 10)
partInfo[0] = strings.Join(startSec, "=")
sizeSec[1] = strconv.FormatUint(p.Size, 10)
partInfo[1] = strings.Join(sizeSec, "=")
content[1] = strings.Join(partInfo, ",")
lines[idx] = strings.Join(content, ":")
}
break
}
if !foundPartition {
return table, fmt.Errorf("cannot find the target partition %q, "+
"partition table: %s", partName, table)
}
if change {
table = strings.Join(lines, "\n")
}
return table, nil
}
// ReadPartitionTable reads the partition table of a disk.
func ReadPartitionTable(disk string) (string, error) {
table, err := exec.Command("sudo", "sfdisk", "--dump", disk).Output()
if err != nil {
return "", fmt.Errorf("cannot dump partition table of %q, "+
"error msg: (%v)", disk, err)
}
return string(table), nil
}
// ReadPartitionSize reads the size of a partition (unit:sectors of 512 Bytes).
func ReadPartitionSize(disk string, partNumInt int) (uint64, error) {
if len(disk) <= 0 || partNumInt <= 0 {
return 0, fmt.Errorf("invalid input: disk=%q, partNumInt=%d", disk, partNumInt)
}
// get partition number string
partNum, err := PartNumIntToString(disk, partNumInt)
if err != nil {
return 0, fmt.Errorf("error in converting partition number, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, partNumInt, err)
}
partName := disk + partNum
table, err := ReadPartitionTable(disk)
if err != nil {
return 0, fmt.Errorf("cannot read partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
var size uint64 = 0
if _, err = HandlePartitionTable(table, partName, false, func(p *PartContent) { size = p.Size }); err != nil {
return 0, fmt.Errorf("error parsing partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
return size, nil
}
// ReadPartitionStart reads the start sector of a partition.
func ReadPartitionStart(disk string, partNumInt int) (uint64, error) {
if len(disk) <= 0 || partNumInt <= 0 {
return 0, fmt.Errorf("invalid input: disk=%q, partNumInt=%d", disk, partNumInt)
}
// get partition number string
partNum, err := PartNumIntToString(disk, partNumInt)
if err != nil {
return 0, fmt.Errorf("error in converting partition number, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, partNumInt, err)
}
partName := disk + partNum
table, err := ReadPartitionTable(disk)
if err != nil {
return 0, fmt.Errorf("cannot read partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
var start uint64 = 0
if _, err = HandlePartitionTable(table, partName, false, func(p *PartContent) { start = p.Start }); err != nil {
return 0, fmt.Errorf("error parsing partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
return start, nil
}
// IsPartitionMinimal determines if a partition is the smallest size it can be.
// If this function returns true, MinimizePartition can make the given partition
// smaller.
func IsPartitionMinimal(disk string, partNumInt int) (bool, error) {
numSectors, err := ReadPartitionSize(disk, partNumInt)
if err != nil {
return false, err
}
if numSectors > minPartitionSizeSectors {
return false, nil
}
return true, nil
}
// MinimizePartition minimizes the input partition and
// returns the next sector of the end sector.
// The smallest partition from fdisk is 1 sector partition.
func MinimizePartition(disk string, partNumInt int) (uint64, error) {
minSize := minPartitionSizeSectors
if len(disk) == 0 || partNumInt <= 0 {
return 0, fmt.Errorf("empty disk name or nonpositive part number, "+
"input: disk=%q, partNumInt=%d", disk, partNumInt)
}
// get partition number string
partNum, err := PartNumIntToString(disk, partNumInt)
if err != nil {
return 0, fmt.Errorf("error in converting partition number, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, partNumInt, err)
}
partName := disk + partNum
var tableBuffer bytes.Buffer
var oldSize uint64
// dump partition table.
table, err := ReadPartitionTable(disk)
if err != nil {
return 0, fmt.Errorf("cannot read partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
var startSector uint64
// edit partition table.
table, err = HandlePartitionTable(table, partName, true, func(p *PartContent) {
startSector = p.Start
oldSize = p.Size
p.Size = minSize
})
if err != nil {
return 0, fmt.Errorf("error when editing partition table of %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
if oldSize <= minSize {
log.Printf("warning: old partition size=%d is smaller than minSize=%d, "+
"nothing is done, "+
"return value is start sector + minSize (%d)", oldSize, minSize, minSize)
return startSector + minSize, nil
}
tableBuffer.WriteString(table)
// write partition table back.
writeTableCmd := exec.Command("sudo", "sfdisk", "--no-reread", disk)
writeTableCmd.Stdin = &tableBuffer
writeTableCmd.Stdout = os.Stdout
writeTableCmd.Stderr = os.Stderr
if err := writeTableCmd.Run(); err != nil {
return 0, fmt.Errorf("error in writing partition table back to %q, "+
"input: disk=%q, partNumInt=%d, "+
"error msg: (%v)", disk, disk, partNumInt, err)
}
log.Printf("\nCompleted minimizing %q\n\n", partName)
return startSector + minSize, nil
}