| // Copyright 2021 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 provisioner |
| |
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "time" |
| |
| "golang.org/x/sys/unix" |
| |
| "cos.googlesource.com/cos/tools.git/src/pkg/tools/partutil" |
| "cos.googlesource.com/cos/tools.git/src/pkg/utils" |
| ) |
| |
| func switchRoot(deps Deps, runState *state) (err error) { |
| if !runState.data.Config.BootDisk.ReclaimSDA3 { |
| log.Println("ReclaimSDA3 is not set, not switching root device") |
| return nil |
| } |
| sda3Device := filepath.Join(deps.RootDir, "dev", "sda3") |
| sda5Device := filepath.Join(deps.RootDir, "dev", "sda5") |
| rootDev, err := exec.Command(deps.RootdevCmd, "-s").Output() |
| if err != nil { |
| if exitErr, ok := err.(*exec.ExitError); ok { |
| return fmt.Errorf("error running rootdev: %v: stderr = %q", exitErr, string(exitErr.Stderr)) |
| } |
| return fmt.Errorf("error running rootdev: %v", err) |
| } |
| if strings.TrimSpace(string(rootDev)) == sda5Device { |
| log.Println("Current root device is /dev/sda5, not switching root device") |
| return nil |
| } |
| log.Println("Need to switch root device") |
| log.Println("Copying sda3 to sda5...") |
| in, err := os.Open(sda3Device) |
| if err != nil { |
| return err |
| } |
| defer utils.CheckClose(in, "error closing /dev/sda3", &err) |
| out, err := os.Create(sda5Device) |
| if err != nil { |
| return err |
| } |
| defer utils.CheckClose(out, "error closing /dev/sda5", &err) |
| if _, err := io.Copy(out, in); err != nil { |
| return fmt.Errorf("error copying from sda3 to sda5: %v", err) |
| } |
| log.Println("Setting GPT priority...") |
| device := filepath.Join(deps.RootDir, "dev", "sda") |
| if err := utils.RunCommand([]string{deps.CgptCmd, "prioritize", "-P", "5", "-i", "4", device}, "", nil); err != nil { |
| return err |
| } |
| log.Println("Reboot required to switch root device") |
| return ErrRebootRequired |
| } |
| |
| func shrinkSDA3(deps Deps, runState *state) error { |
| if !runState.data.Config.BootDisk.ReclaimSDA3 { |
| log.Println("ReclaimSDA3 is not set, not shrinking sda3") |
| return nil |
| } |
| device := filepath.Join(deps.RootDir, "dev", "sda") |
| minimal, err := partutil.IsPartitionMinimal(device, 3) |
| if err != nil { |
| return fmt.Errorf("error checking /dev/sda3 size: %v", err) |
| } |
| if minimal { |
| log.Println("/dev/sda3 is minimally sized, not shrinking sda3") |
| return nil |
| } |
| log.Println("ReclaimSDA3 is set, and /dev/sda3 is not minimal; now shrinking sda3") |
| if _, err := partutil.MinimizePartition(device, 3); err != nil { |
| return fmt.Errorf("error minimizing /dev/sda3: %v", err) |
| } |
| log.Println("Reboot required to reload partition table changes") |
| return ErrRebootRequired |
| } |
| |
| func setupOnShutdownUnit(deps Deps, runState *state) (err error) { |
| if err := mountFunc("", filepath.Join(deps.RootDir, "tmp"), "", unix.MS_REMOUNT|unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil { |
| return fmt.Errorf("error remounting /tmp as exec: %v", err) |
| } |
| if err := ioutil.WriteFile(filepath.Join(deps.RootDir, "tmp", "handle_disk_layout.bin"), deps.HandleDiskLayoutBin, 0744); err != nil { |
| return err |
| } |
| data := fmt.Sprintf(`[Unit] |
| Description=Run after everything unmounted |
| DefaultDependencies=false |
| Conflicts=shutdown.target |
| Before=mnt-stateful_partition.mount usr-share-oem.mount |
| After=tmp.mount |
| |
| [Service] |
| Type=oneshot |
| RemainAfterExit=true |
| ExecStart=/bin/true |
| ExecStop=/bin/bash -c '/tmp/handle_disk_layout.bin /dev/sda 1 8 "%s" "%t" 2>&1 | sed "s/^/BuildStatus: /"' |
| TimeoutStopSec=600 |
| StandardOutput=tty |
| StandardError=tty |
| TTYPath=/dev/ttyS2 |
| `, runState.data.Config.BootDisk.OEMSize, runState.data.Config.BootDisk.ReclaimSDA3) |
| if err := ioutil.WriteFile(filepath.Join(deps.RootDir, "etc/systemd/system/last-run.service"), []byte(data), 0664); err != nil { |
| return err |
| } |
| systemd := systemdClient{systemctl: deps.SystemctlCmd} |
| if err := systemd.start("last-run.service", []string{"--no-block"}); err != nil { |
| return err |
| } |
| // journald needs to be stopped in order for the stateful partition to be |
| // unmounted at shutdown. We need the stateful partition to be unmounted so |
| // that disk repartitioning can occur. |
| if err := systemd.stopJournald(deps.RootDir); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func calcSDA3End(device string) (uint64, error) { |
| sda3Start, err := partutil.ReadPartitionStart(device, 3) |
| if err != nil { |
| return 0, err |
| } |
| sda3Size, err := partutil.ReadPartitionSize(device, 3) |
| if err != nil { |
| return 0, err |
| } |
| sda3End := sda3Start + sda3Size - 1 |
| return sda3End, nil |
| } |
| |
| func waitForDiskResize(deps Deps, runState *state) error { |
| if !runState.data.Config.BootDisk.WaitForDiskResize { |
| log.Println("WaitForDiskResize is not set, not waiting for a boot disk resize") |
| return nil |
| } |
| if runState.data.DiskResizeComplete { |
| log.Println("Already finished waiting for disk resize, not waiting again") |
| return nil |
| } |
| startSize, err := ioutil.ReadFile(filepath.Join(deps.RootDir, "sys/class/block/sda/size")) |
| if err != nil { |
| return err |
| } |
| log.Println("WaitForDiskResize is set; waiting for the boot disk size to change. Timeout is 3 minutes") |
| start := time.Now() |
| end := start.Add(3 * time.Minute) |
| for time.Now().Before(end) { |
| curSize, err := ioutil.ReadFile(filepath.Join(deps.RootDir, "sys/class/block/sda/size")) |
| if err != nil { |
| return err |
| } |
| if string(curSize) != string(startSize) { |
| log.Printf("Boot disk size has changed: start %q, end %q", strings.TrimSpace(string(startSize)), strings.TrimSpace(string(curSize))) |
| runState.data.DiskResizeComplete = true |
| return runState.write() |
| } |
| time.Sleep(time.Second) |
| } |
| return errors.New("timed out waiting for disk resize") |
| } |
| |
| func relocatePartitions(deps Deps, runState *state) error { |
| if !runState.data.Config.BootDisk.ReclaimSDA3 && runState.data.Config.BootDisk.OEMSize == "" { |
| log.Println("ReclaimSDA3 is not set, OEM resize not requested, not relocating partitions") |
| return nil |
| } |
| device := filepath.Join(deps.RootDir, "dev", "sda") |
| if runState.data.Config.BootDisk.OEMSize != "" { |
| // Check if OEM partition is after sda3; if so, then we're done |
| oemStart, err := partutil.ReadPartitionStart(device, 8) |
| if err != nil { |
| return err |
| } |
| sda3End, err := calcSDA3End(device) |
| if err != nil { |
| return err |
| } |
| if oemStart > sda3End { |
| log.Println("OEM resize requested, OEM appears to be relocated after sda3. Partition relocation is complete") |
| return nil |
| } |
| } else { |
| // Check two things: |
| // 1. sda3 is minimal |
| // 2. Stateful partition is located immediately after sda3 |
| // |
| // If both are true, we are done. |
| minimal, err := partutil.IsPartitionMinimal(device, 3) |
| if err != nil { |
| return err |
| } |
| statefulStart, err := partutil.ReadPartitionStart(device, 1) |
| if err != nil { |
| return err |
| } |
| sda3Start, err := partutil.ReadPartitionStart(device, 3) |
| // The stateful partition is relocated 4096 sectors after the start of sda3. |
| // See src/pkg/tools/handle_disk_layout.go for details. |
| if minimal && statefulStart == sda3Start+4096 { |
| log.Println("ReclaimSDA3 is set, sda3 appears to have been reclaimed. Partition relocation is complete") |
| return nil |
| } |
| } |
| // Partition relocation must be done. Prepare for disk relocation to happen on |
| // the next reboot |
| log.Println("Partition relocation is required. Preparing for partition relocation to occur on the next reboot") |
| if err := setupOnShutdownUnit(deps, runState); err != nil { |
| return err |
| } |
| log.Println("Reboot required to relocate partitions") |
| return ErrRebootRequired |
| } |
| |
| func resizeOEMFileSystem(deps Deps, runState *state) error { |
| if !runState.data.Config.BootDisk.ReclaimSDA3 && runState.data.Config.BootDisk.OEMSize == "" { |
| log.Println("ReclaimSDA3 is not set, OEM resize not requested, partition relocation did not occur, FS resize unnecessary") |
| return nil |
| } |
| // Check if OEM partition is after sda3; if so, then relocation occurred and |
| // we need to resize the file system. |
| device := filepath.Join(deps.RootDir, "dev", "sda") |
| sda3End, err := calcSDA3End(device) |
| if err != nil { |
| return err |
| } |
| oemStart, err := partutil.ReadPartitionStart(device, 8) |
| if err != nil { |
| return err |
| } |
| if oemStart < sda3End { |
| log.Println("OEM partition is before sda3; relocation did not occur, FS resize unnecessary") |
| return nil |
| } |
| log.Println("Partition relocation appears to have occurred, resizing the OEM file system") |
| systemd := systemdClient{systemctl: deps.SystemctlCmd} |
| if err := systemd.stop("usr-share-oem.mount"); err != nil { |
| return err |
| } |
| sda8 := filepath.Join(deps.RootDir, "dev", "sda8") |
| if err := utils.RunCommand([]string{deps.E2fsckCmd, "-fp", sda8}, "", nil); err != nil { |
| return err |
| } |
| resizeArgs := []string{deps.Resize2fsCmd, sda8} |
| if runState.data.Config.BootDisk.OEMFSSize4K != 0 { |
| resizeArgs = append(resizeArgs, strconv.FormatUint(runState.data.Config.BootDisk.OEMFSSize4K, 10)) |
| } |
| if err := utils.RunCommand(resizeArgs, "", nil); err != nil { |
| return err |
| } |
| if err := systemd.start("usr-share-oem.mount", nil); err != nil { |
| return err |
| } |
| log.Println("OEM file system resized to account for available space") |
| return nil |
| } |
| |
| // repartitionBootDisk executes all behaviors related to repartitioning the boot |
| // disk. Most of these behaviors require a reboot. To keep reboots simple (e.g. |
| // we don't want to initiate a reboot when deferred statements are unresolved), |
| // we handle reboots by returning ErrRebootRequired and asking the caller to |
| // initiate the reboot. |
| func repartitionBootDisk(deps Deps, runState *state) error { |
| if err := switchRoot(deps, runState); err != nil { |
| return err |
| } |
| if err := shrinkSDA3(deps, runState); err != nil { |
| return err |
| } |
| if err := waitForDiskResize(deps, runState); err != nil { |
| return err |
| } |
| if err := relocatePartitions(deps, runState); err != nil { |
| return err |
| } |
| if err := resizeOEMFileSystem(deps, runState); err != nil { |
| return err |
| } |
| return nil |
| } |