From a12eb767343d0cb449175e18ad12db3bb00eb92d Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Wed, 14 Oct 2020 23:08:31 +0300 Subject: [PATCH] test: add unit-test for the installer manifest This test only works on local machine (see notes in the file). Signed-off-by: Andrey Smirnov --- cmd/installer/pkg/install/manifest.go | 70 +++++++----- cmd/installer/pkg/install/manifest_test.go | 79 +++++++++++++ internal/pkg/loopback/loopback.go | 126 +++++++++++++++++++++ 3 files changed, 245 insertions(+), 30 deletions(-) create mode 100644 internal/pkg/loopback/loopback.go diff --git a/cmd/installer/pkg/install/manifest.go b/cmd/installer/pkg/install/manifest.go index d13580a43..351e30fa4 100644 --- a/cmd/installer/pkg/install/manifest.go +++ b/cmd/installer/pkg/install/manifest.go @@ -60,7 +60,7 @@ func NewManifest(label string, sequence runtime.Sequence, opts *Options) (manife Targets: map[string][]*Target{}, } - // Verify that the target device(s) can satisify the requested options. + // Verify that the target device(s) can satisfy the requested options. if sequence != runtime.SequenceUpgrade { if err = VerifyEphemeralPartition(opts); err != nil { @@ -72,7 +72,7 @@ func NewManifest(label string, sequence runtime.Sequence, opts *Options) (manife } } - // Initialize any slices we need. Note that a boot paritition is not + // Initialize any slices we need. Note that a boot partition is not // required. if manifest.Targets[opts.Disk] == nil { @@ -304,42 +304,52 @@ func (t *Target) Format() error { // Save copies the assets to the bootloader partition. func (t *Target) Save() (err error) { for _, asset := range t.Assets { - var ( - sourceFile *os.File - destFile *os.File - ) + asset := asset - if sourceFile, err = os.Open(asset.Source); err != nil { - return err - } - // nolint: errcheck - defer sourceFile.Close() + err = func() error { + var ( + sourceFile *os.File + destFile *os.File + ) - if err = os.MkdirAll(filepath.Dir(asset.Destination), os.ModeDir); err != nil { - return err - } + if sourceFile, err = os.Open(asset.Source); err != nil { + return err + } + // nolint: errcheck + defer sourceFile.Close() - if destFile, err = os.Create(asset.Destination); err != nil { - return err - } + if err = os.MkdirAll(filepath.Dir(asset.Destination), os.ModeDir); err != nil { + return err + } - // nolint: errcheck - defer destFile.Close() + if destFile, err = os.Create(asset.Destination); err != nil { + return err + } - log.Printf("copying %s to %s\n", sourceFile.Name(), destFile.Name()) + // nolint: errcheck + defer destFile.Close() - if _, err = io.Copy(destFile, sourceFile); err != nil { - log.Printf("failed to copy %s to %s\n", sourceFile.Name(), destFile.Name()) - return err - } + log.Printf("copying %s to %s\n", sourceFile.Name(), destFile.Name()) - if err = destFile.Close(); err != nil { - log.Printf("failed to close %s", destFile.Name()) - return err - } + if _, err = io.Copy(destFile, sourceFile); err != nil { + log.Printf("failed to copy %s to %s\n", sourceFile.Name(), destFile.Name()) + return err + } - if err = sourceFile.Close(); err != nil { - log.Printf("failed to close %s", sourceFile.Name()) + if err = destFile.Close(); err != nil { + log.Printf("failed to close %s", destFile.Name()) + return err + } + + if err = sourceFile.Close(); err != nil { + log.Printf("failed to close %s", sourceFile.Name()) + return err + } + + return nil + }() + + if err != nil { return err } } diff --git a/cmd/installer/pkg/install/manifest_test.go b/cmd/installer/pkg/install/manifest_test.go index 194cedf71..8299a8e33 100644 --- a/cmd/installer/pkg/install/manifest_test.go +++ b/cmd/installer/pkg/install/manifest_test.go @@ -13,17 +13,96 @@ import ( "github.com/stretchr/testify/suite" + "github.com/talos-systems/go-blockdevice/blockdevice" + "github.com/talos-systems/talos/cmd/installer/pkg/install" + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" + "github.com/talos-systems/talos/internal/pkg/loopback" ) +// Some tests in this package cannot be run under buildkit, as buildkit doesn't propagate partition devices +// like /dev/loopXpY into the sandbox. To run the tests on your local computer, do the following: +// +// sudo go test -v --count 1 ./cmd/installer/pkg/install/ + type manifestSuite struct { suite.Suite + + disk *os.File + loopbackDevice *os.File } +const diskSize = 10 * 1024 * 1024 * 1024 * 1024 // 10 GiB + func TestManifestSuite(t *testing.T) { suite.Run(t, new(manifestSuite)) } +func (suite *manifestSuite) SetupSuite() { + var err error + + suite.disk, err = ioutil.TempFile("", "talos") + suite.Require().NoError(err) + + suite.Require().NoError(suite.disk.Truncate(diskSize)) + + suite.loopbackDevice, err = loopback.NextLoopDevice() + suite.Require().NoError(err) + + suite.T().Logf("Using %s", suite.loopbackDevice.Name()) + + suite.Require().NoError(loopback.Loop(suite.loopbackDevice, suite.disk)) + + suite.Require().NoError(loopback.LoopSetReadWrite(suite.loopbackDevice)) +} + +func (suite *manifestSuite) TearDownSuite() { + if suite.loopbackDevice != nil { + suite.Assert().NoError(loopback.Unloop(suite.loopbackDevice)) + } + + if suite.disk != nil { + suite.Assert().NoError(os.Remove(suite.disk.Name())) + suite.Assert().NoError(suite.disk.Close()) + } +} + +func (suite *manifestSuite) skipUnderBuildkit() { + hostname, _ := os.Hostname() //nolint: errcheck + + if hostname == "buildkitsandbox" { + suite.T().Skip("test not supported under buildkit as partition devices are not propagated from /dev") + } +} + +func (suite *manifestSuite) verifyBlockdevice() { + bd, err := blockdevice.Open(suite.loopbackDevice.Name()) + suite.Require().NoError(err) + + defer bd.Close() //nolint: errcheck + + table, err := bd.PartitionTable() + suite.Require().NoError(err) + + suite.Assert().Len(table.Partitions(), 5) + + suite.Assert().NoError(bd.Close()) +} + +func (suite *manifestSuite) TestExecuteManifestClean() { + suite.skipUnderBuildkit() + + manifest, err := install.NewManifest("A", runtime.SequenceInstall, &install.Options{ + Disk: suite.loopbackDevice.Name(), + Force: true, + }) + suite.Require().NoError(err) + + suite.Assert().NoError(manifest.ExecuteManifest()) + + suite.verifyBlockdevice() +} + func (suite *manifestSuite) TestTargetInstall() { // Create Temp dirname for mountpoint dir, err := ioutil.TempDir("", "talostest") diff --git a/internal/pkg/loopback/loopback.go b/internal/pkg/loopback/loopback.go new file mode 100644 index 000000000..1c4450867 --- /dev/null +++ b/internal/pkg/loopback/loopback.go @@ -0,0 +1,126 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package loopback provides support for disk loopback devices (/dev/loopN). +package loopback + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// Copyright (c) 2017, Paul R. Tagliamonte + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// syscalls will return an errno type (which implements error) for all calls, +// including success (errno 0). We only care about non-zero errnos. +func errnoIsErr(err error) error { + if err.(syscall.Errno) != 0 { + return err + } + + return nil +} + +// Loop given a handle to a Loopback device (such as /dev/loop0), and a handle +// to the image to loop mount (such as a squashfs or ext4fs image), performs +// the required call to loop the image to the provided block device. +func Loop(loopbackDevice, image *os.File) error { + _, _, err := syscall.Syscall( + syscall.SYS_IOCTL, + loopbackDevice.Fd(), + unix.LOOP_SET_FD, + image.Fd(), + ) + + return errnoIsErr(err) +} + +// LoopSetReadWrite clears the read-only flag on the loop devices. +func LoopSetReadWrite(loopbackDevice *os.File) error { + var status unix.LoopInfo64 + + _, _, err := syscall.Syscall( + syscall.SYS_IOCTL, + loopbackDevice.Fd(), + unix.LOOP_GET_STATUS64, + uintptr(unsafe.Pointer(&status)), + ) + + if e := errnoIsErr(err); e != nil { + return e + } + + status.Flags &= ^uint32(unix.LO_FLAGS_READ_ONLY) + + _, _, err = syscall.Syscall( + syscall.SYS_IOCTL, + loopbackDevice.Fd(), + unix.LOOP_SET_STATUS64, + uintptr(unsafe.Pointer(&status)), + ) + + runtime.KeepAlive(status) + + return errnoIsErr(err) +} + +// Unloop given a handle to the Loopback device (such as /dev/loop0), preforms the +// required call to the image to unloop the file. +func Unloop(loopbackDevice *os.File) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopbackDevice.Fd(), unix.LOOP_CLR_FD, 0) + + return errnoIsErr(err) +} + +// NextLoopDevice gets the next loopback device that isn't used. +// +// Under the hood this will ask loop-control for the LOOP_CTL_GET_FREE value, and interpolate +// that into the conventional GNU/Linux naming scheme for loopback devices, and os.Open +// that path. +func NextLoopDevice() (*os.File, error) { + loopInt, err := nextUnallocatedLoop() + if err != nil { + return nil, err + } + + return os.OpenFile(fmt.Sprintf("/dev/loop%d", loopInt), os.O_RDWR, 0) +} + +// Return the integer of the next loopback device we can use by calling +// loop-control with the LOOP_CTL_GET_FREE ioctl. +func nextUnallocatedLoop() (int, error) { + fd, err := os.OpenFile("/dev/loop-control", os.O_RDONLY, 0o644) + if err != nil { + return 0, err + } + + defer fd.Close() //nolint: errcheck + + index, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd.Fd(), unix.LOOP_CTL_GET_FREE, 0) + + return int(index), errnoIsErr(err) +}