From 9526336d0101ea80eace0e753d8b4228728250c0 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 14 Oct 2020 15:12:47 +0200 Subject: [PATCH 01/52] Update the changelog --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 51fc38b..4d29999 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +golang-github-go-debos-fakemachine (0.0~git20200805.8defac2-1) unstable; urgency=medium + + * New upstream release. + * Set upstream metadata fields: Bug-Database, Bug-Submit. + * Replace use of deprecated $ADTTMP with $AUTOPKGTEST_TMP. + + -- Andrej Shadura Wed, 14 Oct 2020 15:12:38 +0200 + golang-github-go-debos-fakemachine (0.0~git20200528.83ab90c-1) unstable; urgency=medium * New upstream snapshot. From ebe8abb5667438d5e1d69e97d0d44542cd9ef901 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 2 Dec 2020 13:52:22 +0100 Subject: [PATCH 02/52] Add myself as an uploader --- debian/control | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 2258614..d41b4d8 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,9 @@ Source: golang-github-go-debos-fakemachine Section: devel Priority: optional Maintainer: Debian Go Packaging Team -Uploaders: Héctor Orón Martínez +Uploaders: + Andrej Shadura , + Héctor Orón Martínez Build-Depends: debhelper (>= 10), dh-golang, golang-any, From 91690365a4a1f52603f921f264aa2a112c166118 Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 2 Dec 2020 13:52:34 +0100 Subject: [PATCH 03/52] Bump Standards-Version to 4.5.1 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index d41b4d8..11a7b29 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Build-Depends: debhelper (>= 10), # need for tests # busybox, # qemu-system -Standards-Version: 4.2.1 +Standards-Version: 4.5.1 Rules-Requires-Root: no Homepage: https://github.com/go-debos/fakemachine Vcs-Browser: https://salsa.debian.org/go-team/packages/golang-github-go-debos-fakemachine From a2cd7609a625662edf8454a2632f8b6da12f9b6c Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 2 Dec 2020 13:59:40 +0100 Subject: [PATCH 04/52] Use dh 12 --- debian/compat | 1 - debian/control | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 debian/compat diff --git a/debian/compat b/debian/compat deleted file mode 100644 index f599e28..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -10 diff --git a/debian/control b/debian/control index 11a7b29..0659994 100644 --- a/debian/control +++ b/debian/control @@ -5,7 +5,7 @@ Maintainer: Debian Go Packaging Team , Héctor Orón Martínez -Build-Depends: debhelper (>= 10), +Build-Depends: debhelper-compat (= 12), dh-golang, golang-any, golang-github-docker-go-units-dev, From 7c10bb89b64c66279b7c9c257d6c2a1d8741de6d Mon Sep 17 00:00:00 2001 From: Andrej Shadura Date: Wed, 2 Dec 2020 14:51:42 +0100 Subject: [PATCH 05/52] Update the changelog --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index 4d29999..d5c12ce 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +golang-github-go-debos-fakemachine (0.0~git20201127.9e6ee78-1) unstable; urgency=medium + + * New upstream release. + * Add myself as an uploader. + * Bump Standards-Version to 4.5.1. + * Use dh 12. + + -- Andrej Shadura Wed, 02 Dec 2020 14:51:32 +0100 + golang-github-go-debos-fakemachine (0.0~git20200805.8defac2-1) unstable; urgency=medium * New upstream release. From 6946f54039be11c8546cbc5ed41b9fba086ea7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dylan=20A=C3=AFssi?= Date: Mon, 20 Sep 2021 14:17:12 +0200 Subject: [PATCH 06/52] Bump d/changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dylan Aïssi --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index d5c12ce..002526a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +golang-github-go-debos-fakemachine (0.0~git20210901.fc48786-1) UNRELEASED; urgency=medium + + * Team upload. + * New upstream release. + + -- Dylan Aïssi Mon, 20 Sep 2021 14:16:12 +0200 + golang-github-go-debos-fakemachine (0.0~git20201127.9e6ee78-1) unstable; urgency=medium * New upstream release. From 1cf74555059ca239aa65ef46a2558cf1c71d8c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dylan=20A=C3=AFssi?= Date: Mon, 20 Sep 2021 14:22:18 +0200 Subject: [PATCH 07/52] Remove the final '.' stop character at end of description to make lintian happy --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 0659994..040b411 100644 --- a/debian/control +++ b/debian/control @@ -33,7 +33,7 @@ Depends: busybox | busybox-static, ${misc:Depends} Recommends: e2fsprogs, linux-image-amd64 -Description: create and spawn virtual machines for building images with debos. +Description: create and spawn virtual machines for building images with debos Create and spawn virtual machines for building images with debos tool. Package: golang-github-go-debos-fakemachine-dev @@ -41,6 +41,6 @@ Architecture: amd64 Built-Using: ${misc:Built-Using} Depends: ${shlibs:Depends}, ${misc:Depends} -Description: create and spawn virtual machines for building images with debos. +Description: create and spawn virtual machines for building images with debos Create and spawn virtual machines for building images with debos tool. (development libraries) From 39db09cbdeb162a432db190a47076747b406cb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dylan=20A=C3=AFssi?= Date: Mon, 20 Sep 2021 14:37:30 +0200 Subject: [PATCH 08/52] Upload to unstable --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 002526a..2759693 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,9 @@ -golang-github-go-debos-fakemachine (0.0~git20210901.fc48786-1) UNRELEASED; urgency=medium +golang-github-go-debos-fakemachine (0.0~git20210901.fc48786-1) unstable; urgency=medium * Team upload. * New upstream release. - -- Dylan Aïssi Mon, 20 Sep 2021 14:16:12 +0200 + -- Dylan Aïssi Mon, 20 Sep 2021 14:36:06 +0200 golang-github-go-debos-fakemachine (0.0~git20201127.9e6ee78-1) unstable; urgency=medium From 3448a687280b2885542b1259b607872764b0bdd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alo=C3=AFs=20Micard?= Date: Wed, 1 Dec 2021 11:56:34 +0000 Subject: [PATCH 09/52] [skip ci] update debian/gitlab-ci.yml (using pkg-go-tools/ci-config) See: https://salsa.debian.org/go-team/infra/pkg-go-tools Gbp-Dch: Ignore --- debian/gitlab-ci.yml | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml index 5c8c31b..594e14e 100644 --- a/debian/gitlab-ci.yml +++ b/debian/gitlab-ci.yml @@ -1,28 +1,6 @@ - # auto-generated, DO NOT MODIFY. # The authoritative copy of this file lives at: -# https://salsa.debian.org/go-team/ci/blob/master/cmd/ci/gitlabciyml.go - -# TODO: publish under debian-go-team/ci -image: stapelberg/ci2 - -test_the_archive: - artifacts: - paths: - - before-applying-commit.json - - after-applying-commit.json - script: - # Create an overlay to discard writes to /srv/gopath/src after the build: - - "rm -rf /cache/overlay/{upper,work}" - - "mkdir -p /cache/overlay/{upper,work}" - - "mount -t overlay overlay -o lowerdir=/srv/gopath/src,upperdir=/cache/overlay/upper,workdir=/cache/overlay/work /srv/gopath/src" - - "export GOPATH=/srv/gopath" - - "export GOCACHE=/cache/go" - # Build the world as-is: - - "ci-build -exemptions=/var/lib/ci-build/exemptions.json > before-applying-commit.json" - # Copy this package into the overlay: - - "GBP_CONF_FILES=:debian/gbp.conf gbp buildpackage --git-no-pristine-tar --git-ignore-branch --git-ignore-new --git-export-dir=/tmp/export --git-no-overlay --git-tarball-dir=/nonexistant --git-cleaner=/bin/true --git-builder='dpkg-buildpackage -S -d --no-sign'" - - "pgt-gopath -dsc /tmp/export/*.dsc" - # Rebuild the world: - - "ci-build -exemptions=/var/lib/ci-build/exemptions.json > after-applying-commit.json" - - "ci-diff before-applying-commit.json after-applying-commit.json" +# https://salsa.debian.org/go-team/infra/pkg-go-tools/blob/master/config/gitlabciyml.go +--- +include: + - https://salsa.debian.org/go-team/infra/pkg-go-tools/-/raw/master/pipeline/test-archive.yml From 0f0ff8b0a8814c691bbafe0d9fb20a55f034a02f Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:21:23 +0100 Subject: [PATCH 10/52] New upstream version 0.0.3 --- .github/dependabot.yml | 10 + .github/workflows/ci.yml | 71 +++++++ .golangci.yml | 4 + Jenkinsfile | 30 --- README.md | 23 +++ backend.go | 80 +++++--- backend_kvm.go => backend_qemu.go | 161 ++++++++-------- backend_uml.go | 43 ++--- bors.toml | 2 + cmd/fakemachine/main.go | 32 ++-- cpio/writerhelper.go | 186 +++++++++++++++---- decompressors.go | 48 +++++ decompressors_test.go | 89 +++++++++ go.mod | 13 ++ go.sum | 28 +++ machine.go | 296 ++++++++++++++++++++++-------- machine_test.go | 44 +++-- testdata/test | Bin 0 -> 16384 bytes testdata/test.gz | Bin 0 -> 16412 bytes testdata/test.xz | Bin 0 -> 16444 bytes testdata/test.zst | Bin 0 -> 16398 bytes 21 files changed, 858 insertions(+), 302 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .golangci.yml delete mode 100644 Jenkinsfile create mode 100644 README.md rename backend_kvm.go => backend_qemu.go (69%) create mode 100644 bors.toml create mode 100644 decompressors.go create mode 100644 decompressors_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 testdata/test create mode 100644 testdata/test.gz create mode 100644 testdata/test.xz create mode 100644 testdata/test.zst diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..615dfde --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a51287 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: Build and Test + +on: + push: + branches-ignore: + - '*.tmp' + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + + test: + strategy: + fail-fast: false + matrix: + # Currently nested virtualisation (hence kvm) is not supported on GitHub + # actions; but the qemu backend is enough to test Fakemachine + # functionality without hardware acceleration since the majority of code + # is shared between the qemu and kvm backends. + # See https://github.com/actions/runner-images/issues/183 + # + # For Arch Linux uml is not yet supported, so only test under qemu there. + os: [bullseye, bookworm] + backend: [qemu, uml] + include: + - os: arch + backend: qemu + name: Test ${{matrix.os}} with ${{matrix.backend}} backend + runs-on: ubuntu-latest + defaults: + run: + shell: bash + container: + image: ghcr.io/go-debos/test-containers/fakemachine-${{matrix.os}}:main + options: >- + --security-opt label=disable + --cap-add=SYS_PTRACE + --tmpfs /scratch:exec + env: + TMP: /scratch + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Test build + run: go build -o fakemachine cmd/fakemachine/main.go + + - name: Run unit tests (${{matrix.backend}} backend) + run: go test -v ./... --backend=${{matrix.backend}} | tee test.out + + - name: Ensure no tests were skipped + run: "! grep -q SKIP test.out" + + # Job to key the bors success status against + bors: + name: bors + if: success() + needs: + - golangci + - test + runs-on: ubuntu-latest + steps: + - name: Mark the job as a success + run: exit 0 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..72f3064 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,4 @@ +linters: + enable: + - gofmt + - whitespace diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 48b00c8..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,30 +0,0 @@ -pipeline { - agent { - dockerfile { - args '--device=/dev/kvm' - } - } - environment { - GOPATH="${env.WORKSPACE}/.gopath" - } - stages { - stage("Setup path") { - steps { - sh "mkdir -p .gopath/src/github.com/go-debos" - sh "ln -sf ${env.WORKSPACE} .gopath/src/github.com/go-debos/fakemachine" - sh "go get -v -t -d ./..." - } - } - stage("Run test") { - steps { - sh "go test -v" - } - } - - stage("Test build cmd") { - steps { - sh "go install github.com/go-debos/fakemachine/cmd/fakemachine" - } - } - } -} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fed5766 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# fakemchine - fake a machine + +Creates a vm based on the currently running system. + +## Synopsis + + fakemachine [OPTIONS] + +``` +Application Options: + -b, --backend=[auto|kvm|uml|qemu] Virtualisation backend to use (default: auto) + -v, --volume= volume to mount + -i, --image= image to add + -e, --environ-var= Environment variables (use -e VARIABLE:VALUE syntax) + -m, --memory= Amount of memory for the fakemachine in megabytes + -c, --cpus= Number of CPUs for the fakemachine + -s, --scratchsize= On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, + memory backed scratch space is used + --show-boot Show boot/console messages from the fakemachine + +Help Options: + -h, --help Show this help message +``` diff --git a/backend.go b/backend.go index 948bef6..134ca01 100644 --- a/backend.go +++ b/backend.go @@ -1,41 +1,65 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine -import( +import ( "fmt" ) -// A list of backends which are implemented +// List of backends in order of their priority in the "auto" algorithm +func implementedBackends(m *Machine) []backend { + return []backend{ + newKvmBackend(m), + newUmlBackend(m), + newQemuBackend(m), + } +} + +/* A list of backends which are implemented - sorted in order in which the + * "auto" backend chooses them. + */ func BackendNames() []string { - return []string{"auto", "kvm", "uml"} + names := []string{"auto"} + + for _, backend := range implementedBackends(nil) { + names = append(names, backend.Name()) + } + + return names } +/* The "auto" backend loops through each backend, starting with the lowest order. + * The backend is created and checked if the creation was successful (i.e. it is + * supported on this machine). If so, that backend is used for the fakemachine. If + * unsuccessful, the next backend is created until no more backends remain then + * an error is thrown explaining why each backend was unsuccessful. + */ func newBackend(name string, m *Machine) (backend, error) { + backends := implementedBackends(m) var b backend - - switch name { - case "auto": - // select kvm first - b, kvm_err := newBackend("kvm", m) - if kvm_err == nil { + var err error + + if name == "auto" { + for _, backend := range backends { + backendName := backend.Name() + b, backendErr := newBackend(backendName, m) + if backendErr != nil { + err = fmt.Errorf("%v, %v", err, backendErr) + continue + } return b, nil } + return nil, err + } - // falling back to uml - b, uml_err := newBackend("uml", m) - if uml_err == nil { - return b, nil + // find backend by name + for _, backend := range backends { + if backend.Name() == name { + b = backend } - - // no backend supported - return nil, fmt.Errorf("%v, %v", kvm_err, uml_err) - case "kvm": - b = newKvmBackend(m) - case "uml": - b = newUmlBackend(m) - default: + } + if b == nil { return nil, fmt.Errorf("%s backend does not exist", name) } @@ -58,11 +82,11 @@ type backend interface { // Get kernel release version KernelRelease() (string, error) - // The path to the kernel and modules - KernelPath() (kernelPath string, moddir string, err error) + // The path to the kernel + KernelPath() (kernelPath string, err error) - // A list of modules to include in the initrd - InitrdModules() []string + // The path to the modules + ModulePath() (moddir string, err error) // A list of udev rules UdevRules() []string @@ -76,7 +100,7 @@ type backend interface { // The parameters used to mount a specific volume into the machine MountParameters(mount mountPoint) (fstype string, options []string) - // A list of modules which should be probed in the initscript + // A list of modules to be added to initrd and probed in the initscript InitModules() []string // A list of additional volumes which should mounted in the initscript diff --git a/backend_kvm.go b/backend_qemu.go similarity index 69% rename from backend_kvm.go rename to backend_qemu.go index eacdc0c..423e1a3 100644 --- a/backend_kvm.go +++ b/backend_qemu.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -10,42 +10,36 @@ import ( "os" "os/exec" "path" - "path/filepath" "strings" "golang.org/x/sys/unix" ) -type kvmBackend struct { +type qemuBackend struct { machine *Machine } -func newKvmBackend(m *Machine) kvmBackend { - return kvmBackend{machine: m} +func newQemuBackend(m *Machine) qemuBackend { + return qemuBackend{machine: m} } -func (b kvmBackend) Name() string { - return "kvm" +func (b qemuBackend) Name() string { + return "qemu" } -func (b kvmBackend) Supported() (bool, error) { - kvmDevice, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0); - if err != nil { - return false, err - } - kvmDevice.Close() - +func (b qemuBackend) Supported() (bool, error) { if _, err := b.QemuPath(); err != nil { return false, err } + return true, nil } -func (b kvmBackend) QemuPath() (string, error) { +func (b qemuBackend) QemuPath() (string, error) { return exec.LookPath("qemu-system-x86_64") } -func (b kvmBackend) KernelRelease() (string, error) { +func (b qemuBackend) KernelRelease() (string, error) { /* First try the kernel the current system is running, but if there are no * modules for that try the latest from /lib/modules. The former works best * for systems directly running fakemachine, the latter makes sense in docker @@ -65,7 +59,7 @@ func (b kvmBackend) KernelRelease() (string, error) { return "", err } - for i := len(files)-1; i >= 0; i-- { + for i := len(files) - 1; i >= 0; i-- { /* Ensure the kernel name starts with a digit, in order * to filter out 'extramodules-ARCH' on ArchLinux */ filename := files[i].Name() @@ -77,61 +71,38 @@ func (b kvmBackend) KernelRelease() (string, error) { return "", fmt.Errorf("No kernel found") } -func (b kvmBackend) hostKernelPath(kernelRelease string) (string, error) { - kernelDir := "/boot" - kernelPrefix := "vmlinuz-" - - /* First, try to find a kernel with a well-known name */ - kernelPath := filepath.Join(kernelDir, kernelPrefix + kernelRelease) - if _, err := os.Stat(kernelPath); err == nil { - return kernelPath, nil - } - - /* Otherwise, inspect each kernel installed, and look for the release - * string straight in the binary. Not pretty, but it works. */ - needle := kernelRelease - if !strings.HasSuffix(needle, " ") { - // Add space to match exact kernel description string e.g - // 4.19.0-6-amd64 (debian-kernel@lists.debian.org) #1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11) - needle += " " +func (b qemuBackend) KernelPath() (string, error) { + /* First we look within the modules directory, as supported by + * various distributions - Arch, Fedora... + * + * ... perhaps because systemd requires it to allow hibernation + * https://github.com/systemd/systemd/commit/edda44605f06a41fb86b7ab8128dcf99161d2344 + */ + if moddir, err := b.ModulePath(); err == nil { + kernelPath := path.Join(moddir, "vmlinuz") + if _, err := os.Stat(kernelPath); err == nil { + return kernelPath, nil + } } - files, err := ioutil.ReadDir(kernelDir) + /* Fall-back to the previous method and look in /boot */ + kernelRelease, err := b.KernelRelease() if err != nil { return "", err } - for _, f := range files { - if !strings.HasPrefix(f.Name(), kernelPrefix) || f.IsDir() { - continue - } - - kernelPath := filepath.Join(kernelDir, f.Name()) - buf, err := ioutil.ReadFile(kernelPath) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to read kernel:", err) - continue - } - - if !bytes.Contains(buf, []byte(needle)) { - continue - } - - return kernelPath, nil + kernelPath := "/boot/vmlinuz-" + kernelRelease + if _, err := os.Stat(kernelPath); err != nil { + return "", err } - return "", fmt.Errorf("No kernel found for release %s", kernelRelease) + return kernelPath, nil } -func (b kvmBackend) KernelPath() (string, string, error) { +func (b qemuBackend) ModulePath() (string, error) { kernelRelease, err := b.KernelRelease() if err != nil { - return "", "", err - } - - kernelPath, err := b.hostKernelPath(kernelRelease) - if err != nil { - return "", "", err + return "", err } moddir := "/lib/modules" @@ -141,22 +112,13 @@ func (b kvmBackend) KernelPath() (string, string, error) { moddir = path.Join(moddir, kernelRelease) if _, err := os.Stat(moddir); err != nil { - return "", "", err + return "", err } - return kernelPath, moddir, nil -} - -func (b kvmBackend) InitrdModules() []string { - return []string{"virtio_console", - "virtio", - "virtio_pci", - "virtio_ring", - "9p", - "9pnet_virtio"} + return moddir, nil } -func (b kvmBackend) UdevRules() []string { +func (b qemuBackend) UdevRules() []string { udevRules := []string{} // create symlink under /dev/disk/by-fakemachine-label/ for each virtual image @@ -169,11 +131,11 @@ func (b kvmBackend) UdevRules() []string { return udevRules } -func (b kvmBackend) NetworkdMatch() string { +func (b qemuBackend) NetworkdMatch() string { return "e*" } -func (b kvmBackend) JobOutputTTY() string { +func (b qemuBackend) JobOutputTTY() string { // By default we send job output to the second virtio console, // reserving /dev/ttyS0 for boot messages (which we ignore) // and /dev/hvc0 for possible use by systemd as a getty @@ -186,36 +148,45 @@ func (b kvmBackend) JobOutputTTY() string { return "/dev/hvc0" } -func (b kvmBackend) MountParameters(mount mountPoint) (string, []string) { +func (b qemuBackend) MountParameters(mount mountPoint) (string, []string) { return "9p", []string{"trans=virtio", "version=9p2000.L", "cache=loose", "msize=262144"} } -func (b kvmBackend) InitModules() []string { +func (b qemuBackend) InitModules() []string { return []string{"virtio_pci", "virtio_console", "9pnet_virtio", "9p"} } -func (b kvmBackend) InitStaticVolumes() []mountPoint { +func (b qemuBackend) InitStaticVolumes() []mountPoint { return []mountPoint{} } -func (b kvmBackend) Start() (bool, error) { +func (b qemuBackend) Start() (bool, error) { + return b.StartQemu(false) +} + +func (b qemuBackend) StartQemu(kvm bool) (bool, error) { m := b.machine - kernelPath, _, err := b.KernelPath() + kernelPath, err := b.KernelPath() if err != nil { return false, err } memory := fmt.Sprintf("%d", m.memory) numcpus := fmt.Sprintf("%d", m.numcpus) qemuargs := []string{"qemu-system-x86_64", - "-cpu", "host", "-smp", numcpus, "-m", memory, - "-enable-kvm", "-kernel", kernelPath, "-initrd", m.initrdpath, "-display", "none", "-no-reboot"} + + if kvm { + qemuargs = append(qemuargs, + "-cpu", "host", + "-enable-kvm") + } + kernelargs := []string{"console=ttyS0", "panic=-1", "systemd.unit=fakemachine.service"} @@ -281,3 +252,29 @@ func (b kvmBackend) Start() (bool, error) { return pstate.Success(), nil } + +type kvmBackend struct { + qemuBackend +} + +func newKvmBackend(m *Machine) kvmBackend { + return kvmBackend{qemuBackend{machine: m}} +} + +func (b kvmBackend) Name() string { + return "kvm" +} + +func (b kvmBackend) Supported() (bool, error) { + kvmDevice, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0) + if err != nil { + return false, err + } + kvmDevice.Close() + + return b.qemuBackend.Supported() +} + +func (b kvmBackend) Start() (bool, error) { + return b.StartQemu(true) +} diff --git a/backend_uml.go b/backend_uml.go index ef398d9..3f0bc6e 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -28,8 +28,12 @@ func (b umlBackend) Name() string { func (b umlBackend) Supported() (bool, error) { // check the kernel exists - _, _, err := b.KernelPath() - if err != nil { + if _, err := b.KernelPath(); err != nil { + return false, err + } + + // check the modules exist + if _, err := b.ModulePath(); err != nil { return false, err } @@ -44,41 +48,40 @@ func (b umlBackend) KernelRelease() (string, error) { return "", errors.New("Not implemented") } -func (b umlBackend) KernelPath() (string, string, error) { +func (b umlBackend) KernelPath() (string, error) { // find the UML binary kernelPath, err := exec.LookPath("linux.uml") if err != nil { - return "", "", fmt.Errorf("user-mode-linux not installed") + return "", fmt.Errorf("user-mode-linux not installed") } + return kernelPath, nil +} +func (b umlBackend) ModulePath() (string, error) { // make sure the UML modules exist // on non-merged usr systems the modules still reside under /usr/lib/uml moddir := "/usr/lib/uml/modules" if _, err := os.Stat(moddir); err != nil { - return "", "", fmt.Errorf("user-mode-linux modules not installed") + return "", fmt.Errorf("user-mode-linux modules not installed") } // find the subdirectory containing the modules for the UML release modSubdirs, err := ioutil.ReadDir(moddir) if err != nil { - return "", "", err + return "", err } if len(modSubdirs) != 1 { - return "", "", fmt.Errorf("could not determine which user-mode-linux modules to use") + return "", fmt.Errorf("could not determine which user-mode-linux modules to use") } moddir = path.Join(moddir, modSubdirs[0].Name()) - return kernelPath, moddir, nil + return moddir, nil } func (b umlBackend) SlirpHelperPath() (string, error) { return exec.LookPath("libslirp-helper") } -func (b umlBackend) InitrdModules() []string { - return []string{} -} - func (b umlBackend) UdevRules() []string { udevRules := []string{} @@ -117,7 +120,7 @@ func (b umlBackend) InitModules() []string { func (b umlBackend) InitStaticVolumes() []mountPoint { // mount the UML modules over the top of /lib/modules // which currently contains the modules from the base system - _, moddir, _ := b.KernelPath() + moddir, _ := b.ModulePath() moddir = path.Join(moddir, "../") machineDir := "/lib/modules" @@ -132,7 +135,7 @@ func (b umlBackend) InitStaticVolumes() []mountPoint { func (b umlBackend) Start() (bool, error) { m := b.machine - kernelPath, _, err := b.KernelPath() + kernelPath, err := b.KernelPath() if err != nil { return false, err } @@ -170,10 +173,9 @@ func (b umlBackend) Start() (bool, error) { } defer umlVectorTransportSocket.Close() - // launch libslirp-helper slirpHelperArgs := []string{"libslirp-helper", - "--exit-with-parent"} + "--exit-with-parent"} /* attach the slirpHelperSocket as an additional fd to the process, * after std*. The helper then bridges the host network to the attached @@ -190,8 +192,7 @@ func (b umlBackend) Start() (bool, error) { if err != nil { return false, err } - defer slirpHelper.Kill() - + defer func() { _ = slirpHelper.Kill() }() // launch uml guest memory := fmt.Sprintf("%d", m.memory) @@ -229,7 +230,7 @@ func (b umlBackend) Start() (bool, error) { umlargs = append(umlargs, "con1=fd:0,fd:1", "con0=null", - "con=none") // no other consoles + "con=none") // no other consoles } for i, img := range m.images { diff --git a/bors.toml b/bors.toml new file mode 100644 index 0000000..1db5825 --- /dev/null +++ b/bors.toml @@ -0,0 +1,2 @@ +status = [ "bors" ] +delete_merged_branches = true diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index d9598b6..f2865c0 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -10,14 +10,14 @@ import ( ) type Options struct { - Backend string `short:"b" long:"backend" description:"Virtualisation backend to use" default:"auto"` - Volumes []string `short:"v" long:"volume" description:"volume to mount"` - Images []string `short:"i" long:"image" description:"image to add"` + Backend string `short:"b" long:"backend" description:"Virtualisation backend to use" default:"auto"` + Volumes []string `short:"v" long:"volume" description:"volume to mount"` + Images []string `short:"i" long:"image" description:"image to add"` EnvironVars map[string]string `short:"e" long:"environ-var" description:"Environment variables (use -e VARIABLE:VALUE syntax)"` - Memory int `short:"m" long:"memory" description:"Amount of memory for the fakemachine in megabytes"` - CPUs int `short:"c" long:"cpus" description:"Number of CPUs for the fakemachine"` - ScratchSize string `short:"s" long:"scratchsize" description:"On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, memory backed scratch space is used"` - ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fakemachine"` + Memory int `short:"m" long:"memory" description:"Amount of memory for the fakemachine in megabytes"` + CPUs int `short:"c" long:"cpus" description:"Number of CPUs for the fakemachine"` + ScratchSize string `short:"s" long:"scratchsize" description:"On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, memory backed scratch space is used"` + ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fakemachine"` } var options Options @@ -29,8 +29,8 @@ func warnLocalhost(variable string, value string) { Consider using an address that is valid on your network.` if strings.Contains(value, "localhost") || - strings.Contains(value, "127.0.0.1") || - strings.Contains(value, "::1") { + strings.Contains(value, "127.0.0.1") || + strings.Contains(value, "::1") { fmt.Printf(message, variable) } } @@ -88,7 +88,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { // These are the environment variables that will be detected on the // host and propagated to fakemachine. These are listed lower case, but // they are detected and configured in both lower case and upper case. - var environ_vars = [...]string { + var environ_vars = [...]string{ "http_proxy", "https_proxy", "ftp_proxy", @@ -123,14 +123,12 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { } // Puts in a format that is compatible with output of os.Environ() - if EnvironVars != nil { - EnvironString := []string{} - for k, v := range EnvironVars { - warnLocalhost(k, v) - EnvironString = append(EnvironString, fmt.Sprintf("%s=%s", k, v)) - } - m.SetEnviron(EnvironString) // And save the resulting environ vars on m + EnvironString := []string{} + for k, v := range EnvironVars { + warnLocalhost(k, v) + EnvironString = append(EnvironString, fmt.Sprintf("%s=%s", k, v)) } + m.SetEnviron(EnvironString) // And save the resulting environ vars on m } func main() { diff --git a/cpio/writerhelper.go b/cpio/writerhelper.go index f8a69dc..72cce4d 100644 --- a/cpio/writerhelper.go +++ b/cpio/writerhelper.go @@ -1,8 +1,9 @@ package writerhelper import ( + "bytes" + "fmt" "io" - "log" "os" "path" "path/filepath" @@ -16,6 +17,19 @@ type WriterHelper struct { *cpio.Writer } +type WriteDirectory struct { + Directory string + Perm os.FileMode +} + +type WriteSymlink struct { + Target string + Link string + Perm os.FileMode +} + +type Transformer func(dst io.Writer, src io.Reader) error + func NewWriterHelper(f io.Writer) *WriterHelper { return &WriterHelper{ paths: map[string]bool{"/": true}, @@ -23,11 +37,11 @@ func NewWriterHelper(f io.Writer) *WriterHelper { } } -func (w *WriterHelper) ensureBaseDirectory(directory string) { +func (w *WriterHelper) ensureBaseDirectory(directory string) error { d := path.Clean(directory) if w.paths[d] { - return + return nil } components := strings.Split(directory, "/") @@ -39,12 +53,30 @@ func (w *WriterHelper) ensureBaseDirectory(directory string) { continue } - w.WriteDirectory(collector, 0755) + err := w.WriteDirectory(collector, 0755) + if err != nil { + return err + } + } + + return nil +} + +func (w *WriterHelper) WriteDirectories(directories []WriteDirectory) error { + for _, d := range directories { + err := w.WriteDirectory(d.Directory, d.Perm) + if err != nil { + return err + } } + return nil } -func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(directory)) +func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(directory)) + if err != nil { + return err + } hdr := new(cpio.Header) @@ -52,17 +84,24 @@ func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) { hdr.Name = directory hdr.Mode = int64(perm) - w.WriteHeader(hdr) + err = w.WriteHeader(hdr) + if err != nil { + return err + } w.paths[directory] = true + return nil } -func (w *WriterHelper) WriteFile(file, content string, perm os.FileMode) { - w.WriteFileRaw(file, []byte(content), perm) +func (w *WriterHelper) WriteFile(file, content string, perm os.FileMode) error { + return w.WriteFileRaw(file, []byte(content), perm) } -func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(file)) +func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(file)) + if err != nil { + return err + } hdr := new(cpio.Header) @@ -71,12 +110,30 @@ func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) hdr.Mode = int64(perm) hdr.Size = int64(len(bytes)) - w.WriteHeader(hdr) - w.Write(bytes) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + _, err = w.Write(bytes) + return err +} + +func (w *WriterHelper) WriteSymlinks(links []WriteSymlink) error { + for _, l := range links { + err := w.WriteSymlink(l.Target, l.Link, l.Perm) + if err != nil { + return err + } + } + return nil } -func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(link)) +func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(link)) + if err != nil { + return err + } + hdr := new(cpio.Header) content := []byte(target) @@ -86,13 +143,20 @@ func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) { hdr.Mode = int64(perm) hdr.Size = int64(len(content)) - w.WriteHeader(hdr) - w.Write(content) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = w.Write(content) + return err } -func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, - perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(device)) +func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(device)) + if err != nil { + return err + } hdr := new(cpio.Header) hdr.Type = cpio.TYPE_CHAR @@ -101,32 +165,39 @@ func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, hdr.Devmajor = major hdr.Devminor = minor - w.WriteHeader(hdr) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + return nil } -func (w *WriterHelper) CopyTree(path string) { - walker := func(p string, info os.FileInfo, err error) error { +func (w *WriterHelper) CopyTree(path string) error { + walker := func(p string, info os.FileInfo, _ error) error { + var err error if info.Mode().IsDir() { - w.WriteDirectory(p, info.Mode() & ^os.ModeType) + err = w.WriteDirectory(p, info.Mode() & ^os.ModeType) } else if info.Mode().IsRegular() { - w.CopyFile(p) + err = w.CopyFile(p) } else { - panic("No handled") + err = fmt.Errorf("file type not handled for %s", p) } - return nil + return err } - filepath.Walk(path, walker) + return filepath.Walk(path, walker) } func (w *WriterHelper) CopyFileTo(src, dst string) error { - w.ensureBaseDirectory(path.Dir(dst)) + err := w.ensureBaseDirectory(path.Dir(dst)) + if err != nil { + return err + } f, err := os.Open(src) if err != nil { - log.Panicf("open failed: %s - %v", src, err) - return err + return fmt.Errorf("open failed: %s - %v", src, err) } defer f.Close() @@ -142,8 +213,57 @@ func (w *WriterHelper) CopyFileTo(src, dst string) error { hdr.Mode = int64(info.Mode() & ^os.ModeType) hdr.Size = info.Size() - w.WriteHeader(hdr) - io.Copy(w, f) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = io.Copy(w, f) + if err != nil { + return err + } + + return nil +} + +func (w *WriterHelper) TransformFileTo(src, dst string, fn Transformer) error { + err := w.ensureBaseDirectory(path.Dir(dst)) + if err != nil { + return err + } + + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return err + } + + out := new(bytes.Buffer) + err = fn(out, f) + if err != nil { + return err + } + + hdr := new(cpio.Header) + hdr.Type = cpio.TYPE_REG + hdr.Name = dst + hdr.Mode = int64(info.Mode() & ^os.ModeType) + hdr.Size = int64(out.Len()) + + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = io.Copy(w, out) + if err != nil { + return err + } return nil } diff --git a/decompressors.go b/decompressors.go new file mode 100644 index 0000000..05edcc2 --- /dev/null +++ b/decompressors.go @@ -0,0 +1,48 @@ +package fakemachine + +import ( + "compress/gzip" + "io" + + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +func ZstdDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := zstd.NewReader(src) + if err != nil { + return err + } + defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func XzDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := xz.NewReader(src) + if err != nil { + return err + } + // There is no Close() API. See: https://github.com/ulikunitz/xz/issues/45 + //defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func GzipDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := gzip.NewReader(src) + if err != nil { + return err + } + defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func NullDecompressor(dst io.Writer, src io.Reader) error { + _, err := io.Copy(dst, src) + return err +} diff --git a/decompressors_test.go b/decompressors_test.go new file mode 100644 index 0000000..8b1a77e --- /dev/null +++ b/decompressors_test.go @@ -0,0 +1,89 @@ +package fakemachine + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "path" + "testing" + + "github.com/go-debos/fakemachine/cpio" +) + +func checkStreamsMatch(t *testing.T, output, check io.Reader) error { + i := 0 + oreader := bufio.NewReader(output) + creader := bufio.NewReader(check) + for { + ochar, oerr := oreader.ReadByte() + cchar, cerr := creader.ReadByte() + if oerr != nil || cerr != nil { + if oerr == io.EOF && cerr == io.EOF { + return nil + } + if oerr != nil && oerr != io.EOF { + t.Errorf("Error reading output stream: %s", oerr) + return oerr + } + if cerr != nil && cerr != io.EOF { + t.Errorf("Error reading check stream: %s", cerr) + return cerr + } + return nil + } + + if ochar != cchar { + t.Errorf("Mismatch at byte %d, values %d (output) and %d (check)", + i, ochar, cchar) + return errors.New("Data mismatch") + } + i += 1 + } +} + +func decompressorTest(t *testing.T, file, suffix string, d writerhelper.Transformer) { + f, err := os.Open(path.Join("testdata", file+suffix)) + if err != nil { + t.Errorf("Unable to open test data: %s", err) + return + } + defer f.Close() + + output := new(bytes.Buffer) + err = d(output, f) + if err != nil { + t.Errorf("Error whilst decompressing test file: %s", err) + return + } + + check_f, err := os.Open(path.Join("testdata", file)) + if err != nil { + t.Errorf("Unable to open check data: %s", err) + return + } + defer check_f.Close() + + err = checkStreamsMatch(t, output, check_f) + if err != nil { + t.Errorf("Failed to compare streams: %s", err) + return + } +} + +func TestZstd(t *testing.T) { + decompressorTest(t, "test", ".zst", ZstdDecompressor) +} + +func TestXz(t *testing.T) { + decompressorTest(t, "test", ".xz", XzDecompressor) +} + +func TestGzip(t *testing.T) { + decompressorTest(t, "test", ".gz", GzipDecompressor) +} + +func TestNull(t *testing.T) { + decompressorTest(t, "test", "", NullDecompressor) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d0daba --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/go-debos/fakemachine + +go 1.15 + +require ( + github.com/docker/go-units v0.5.0 + github.com/jessevdk/go-flags v1.5.0 + github.com/klauspost/compress v1.15.3 + github.com/stretchr/testify v1.8.0 + github.com/surma/gocpio v1.1.0 + github.com/ulikunitz/xz v0.5.10 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b1d9995 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/klauspost/compress v1.15.3 h1:wmfu2iqj9q22SyMINp1uQ8C2/V4M1phJdmH9fG4nba0= +github.com/klauspost/compress v1.15.3/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/surma/gocpio v1.1.0 h1:RUWT+VqJ8GSodSv7Oh5xjIxy7r24CV1YvothHFfPxcQ= +github.com/surma/gocpio v1.1.0/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9LCVI= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/machine.go b/machine.go index f1a0189..ae23a67 100644 --- a/machine.go +++ b/machine.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -24,11 +24,7 @@ import ( func mergedUsrSystem() bool { f, _ := os.Lstat("/bin") - if (f.Mode() & os.ModeSymlink) == os.ModeSymlink { - return true - } - - return false + return (f.Mode() & os.ModeSymlink) == os.ModeSymlink } // Parse modinfo output and return the value of module attributes @@ -55,7 +51,7 @@ func getModData(modname string, fieldname string, kernelRelease string) []string // Get full path of module func getModPath(modname string, kernelRelease string) string { path := getModData(modname, "filename", kernelRelease) - if len(path) != 0 { + if len(path) != 0 { return path[0] } return "" @@ -66,7 +62,7 @@ func getModDepends(modname string, kernelRelease string) []string { deplist := getModData(modname, "depends", kernelRelease) var modlist []string for _, v := range deplist { - if v != "" { + if v != "" { modlist = append(modlist, strings.Split(v, ",")...) } } @@ -74,6 +70,13 @@ func getModDepends(modname string, kernelRelease string) []string { return modlist } +var suffixes = map[string]writerhelper.Transformer{ + ".ko": NullDecompressor, + ".ko.gz": GzipDecompressor, + ".ko.xz": XzDecompressor, + ".ko.zst": ZstdDecompressor, +} + func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copiedModules map[string]bool) error { release, _ := m.backend.KernelRelease() modpath := getModPath(modname, release) @@ -90,11 +93,30 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi prefix = "/usr" } - if err := w.CopyFile(prefix + modpath); err != nil { - return err + found := false + for suffix, fn := range suffixes { + if strings.HasSuffix(modpath, suffix) { + // File must exist as-is on the filesystem. Aka do not + // fallback to other suffixes. + if _, err := os.Stat(modpath); err != nil { + return err + } + + // The suffix is the complete thing - ".ko.foobar" + // Reinstate the required ".ko" part, after trimming. + basepath := strings.TrimSuffix(modpath, suffix) + ".ko" + if err := w.TransformFileTo(modpath, prefix+basepath, fn); err != nil { + return err + } + found = true + break + } + } + if !found { + return errors.New("Module extension/suffix unknown") } - copiedModules[modname] = true; + copiedModules[modname] = true deplist := getModDepends(modname, release) for _, mod := range deplist { @@ -150,12 +172,8 @@ type Machine struct { } // Create a new machine object with the auto backend -func NewMachine() *Machine { - m, err := NewMachineWithBackend("auto") - if err != nil { - panic(err) - } - return m +func NewMachine() (*Machine, error) { + return NewMachineWithBackend("auto") } // Create a new machine object @@ -314,9 +332,9 @@ func tmplStaticVolumes(m Machine) []mountPoint { return mounts } -func executeInitScriptTemplate(m *Machine, b backend) []byte { +func executeInitScriptTemplate(m *Machine, b backend) ([]byte, error) { helperFuncs := template.FuncMap{ - "MountVolume": tmplMountVolume, + "MountVolume": tmplMountVolume, "StaticVolumes": tmplStaticVolumes, } @@ -329,9 +347,9 @@ func executeInitScriptTemplate(m *Machine, b backend) []byte { tmpl := template.Must(template.New("init").Funcs(helperFuncs).Parse(initScript)) out := &bytes.Buffer{} if err := tmpl.Execute(out, tmplVariables); err != nil { - panic(err) + return nil, err } - return out.Bytes() + return out.Bytes(), nil } func (m *Machine) addStaticVolume(directory, label string) { @@ -440,7 +458,7 @@ func (m *Machine) SetScratch(scratchsize int64, path string) { } } -func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) { +func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) error { fstab := []string{"# Generated fstab file by fakemachine"} if m.scratchfile == "" { @@ -458,30 +476,54 @@ func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) { } fstab = append(fstab, "") - w.WriteFile("/etc/fstab", strings.Join(fstab, "\n"), 0755) + err := w.WriteFile("/etc/fstab", strings.Join(fstab, "\n"), 0755) + return err +} + +func stripCompressionSuffix(module string) (string, error) { + for suffix := range suffixes { + if strings.HasSuffix(module, suffix) { + // The suffix is the complete thing - ".ko.foobar" + // Reinstate the required ".ko" part, after trimming. + return strings.TrimSuffix(module, suffix) + ".ko", nil + } + } + return "", errors.New("Module extension/suffix unknown") +} + +func (m *Machine) generateModulesDep(w *writerhelper.WriterHelper, moddir string, modules map[string]bool) error { + output := make([]string, len(modules)) + release, _ := m.backend.KernelRelease() + i := 0 + for mod := range modules { + modpath, _ := stripCompressionSuffix(getModPath(mod, release)) // CANNOT fail + deplist := getModDepends(mod, release) // CANNOT fail + deps := make([]string, len(deplist)) + for j, dep := range deplist { + deppath, _ := stripCompressionSuffix(getModPath(dep, release)) // CANNOT fail + deps[j] = deppath + } + output[i] = fmt.Sprintf("%s: %s", modpath, strings.Join(deps, " ")) + i += 1 + } + + path := path.Join(moddir, "modules.dep") + return w.WriteFile(path, strings.Join(output, "\n"), 0644) } func (m *Machine) SetEnviron(environ []string) { m.Environ = environ } - func (m *Machine) writerKernelModules(w *writerhelper.WriterHelper, moddir string, modules []string) error { if len(modules) == 0 { return nil } - modfiles := []string {"modules.order", - "modules.builtin", - "modules.dep", - "modules.dep.bin", - "modules.alias", - "modules.alias.bin", - "modules.softdep", - "modules.symbols", - "modules.symbols.bin", - "modules.builtin.bin", - "modules.devname"} + modfiles := []string{ + "modules.builtin", + "modules.alias", + "modules.symbols"} for _, v := range modfiles { if err := w.CopyFile(moddir + "/" + v); err != nil { @@ -491,12 +533,13 @@ func (m *Machine) writerKernelModules(w *writerhelper.WriterHelper, moddir strin copiedModules := make(map[string]bool) - for _, modname := range modules { + for _, modname := range modules { if err := m.copyModules(w, modname, copiedModules); err != nil { return err } } - return nil + + return m.generateModulesDep(w, moddir, copiedModules) } func (m *Machine) setupscratch() error { @@ -533,7 +576,7 @@ func (m *Machine) cleanup() { func (m *Machine) startup(command string, extracontent [][2]string) (int, error) { defer m.cleanup() - os.Setenv("PATH", os.Getenv("PATH") + ":/sbin:/usr/sbin") + os.Setenv("PATH", os.Getenv("PATH")+":/sbin:/usr/sbin") tmpdir, err := ioutil.TempDir("", "fakemachine-") if err != nil { @@ -556,35 +599,52 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) backend := m.backend - _, kernelModuleDir, err := backend.KernelPath() + kernelModuleDir, err := backend.ModulePath() if err != nil { return -1, err } w := writerhelper.NewWriterHelper(f) - w.WriteDirectory("/scratch", 01777) - w.WriteDirectory("/var/tmp", 01777) - w.WriteDirectory("/var/lib/dbus", 0755) - - w.WriteDirectory("/tmp", 01777) - w.WriteDirectory("/sys", 0755) - w.WriteDirectory("/proc", 0755) - w.WriteDirectory("/run", 0755) - w.WriteDirectory("/usr", 0755) - w.WriteDirectory("/usr/bin", 0755) - w.WriteDirectory("/lib64", 0755) + err = w.WriteDirectories([]writerhelper.WriteDirectory{ + {Directory: "/scratch", Perm: 01777}, + {Directory: "/var/tmp", Perm: 01777}, + {Directory: "/var/lib/dbus", Perm: 0755}, + {Directory: "/tmp", Perm: 01777}, + {Directory: "/sys", Perm: 0755}, + {Directory: "/proc", Perm: 0755}, + {Directory: "/run", Perm: 0755}, + {Directory: "/usr", Perm: 0755}, + {Directory: "/usr/bin", Perm: 0755}, + {Directory: "/lib64", Perm: 0755}, + }) + if err != nil { + return -1, err + } - w.WriteSymlink("/run", "/var/run", 0755) + err = w.WriteSymlink("/run", "/var/run", 0755) + if err != nil { + return -1, err + } if mergedUsrSystem() { - w.WriteSymlink("/usr/sbin", "/sbin", 0755) - w.WriteSymlink("/usr/bin", "/bin", 0755) - w.WriteSymlink("/usr/lib", "/lib", 0755) + err = w.WriteSymlinks([]writerhelper.WriteSymlink{ + {Target: "/usr/sbin", Link: "/sbin", Perm: 0755}, + {Target: "/usr/bin", Link: "/bin", Perm: 0755}, + {Target: "/usr/lib", Link: "/lib", Perm: 0755}, + }) + if err != nil { + return -1, err + } } else { - w.WriteDirectory("/sbin", 0755) - w.WriteDirectory("/bin", 0755) - w.WriteDirectory("/lib", 0755) + err = w.WriteDirectories([]writerhelper.WriteDirectory{ + {Directory: "/sbin", Perm: 0744}, + {Directory: "/bin", Perm: 0755}, + {Directory: "/lib", Perm: 0755}, + }) + if err != nil { + return -1, err + } } prefix := "" @@ -597,63 +657,139 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) if err != nil { return -1, err } - w.CopyFileTo(busybox, prefix + "/bin/busybox") + err = w.CopyFileTo(busybox, prefix+"/bin/busybox") + if err != nil { + return -1, err + } /* Amd64 dynamic linker */ - w.CopyFile("/lib64/ld-linux-x86-64.so.2") + err = w.CopyFile("/lib64/ld-linux-x86-64.so.2") + if err != nil { + return -1, err + } /* C libraries */ libraryDir, err := realDir("/lib64/ld-linux-x86-64.so.2") if err != nil { return -1, err } - w.CopyFile(libraryDir + "/libc.so.6") - w.CopyFile(libraryDir + "/libresolv.so.2") + err = w.CopyFile(libraryDir + "/libc.so.6") + if err != nil { + return -1, err + } + err = w.CopyFile(libraryDir + "/libresolv.so.2") + if err != nil { + return -1, err + } - w.WriteCharDevice("/dev/console", 5, 1, 0700) + err = w.WriteCharDevice("/dev/console", 5, 1, 0700) + if err != nil { + return -1, err + } // Linker configuration - w.CopyFile("/etc/ld.so.conf") - w.CopyTree("/etc/ld.so.conf.d") + err = w.CopyFile("/etc/ld.so.conf") + if err != nil { + return -1, err + } + + err = w.CopyTree("/etc/ld.so.conf.d") + if err != nil { + return -1, err + } // Core system configuration - w.WriteFile("/etc/machine-id", "", 0444) - w.WriteFile("/etc/hostname", "fakemachine", 0444) + err = w.WriteFile("/etc/machine-id", "", 0444) + if err != nil { + return -1, err + } + + err = w.WriteFile("/etc/hostname", "fakemachine", 0444) + if err != nil { + return -1, err + } - w.CopyFile("/etc/passwd") - w.CopyFile("/etc/group") - w.CopyFile("/etc/nsswitch.conf") + err = w.CopyFile("/etc/passwd") + if err != nil { + return -1, err + } + + err = w.CopyFile("/etc/group") + if err != nil { + return -1, err + } + + err = w.CopyFile("/etc/nsswitch.conf") + if err != nil { + return -1, err + } // udev rules udevRules := strings.Join(backend.UdevRules(), "\n") + "\n" - w.WriteFile("/etc/udev/rules.d/61-fakemachine.rules", udevRules, 0444) + err = w.WriteFile("/etc/udev/rules.d/61-fakemachine.rules", udevRules, 0444) + if err != nil { + return -1, err + } - w.WriteFile("/etc/systemd/network/ethernet.network", + err = w.WriteFile("/etc/systemd/network/ethernet.network", fmt.Sprintf(networkdTemplate, backend.NetworkdMatch()), 0444) - w.WriteSymlink( + if err != nil { + return -1, err + } + + err = w.WriteSymlink( "/lib/systemd/resolv.conf", "/etc/resolv.conf", 0755) + if err != nil { + return -1, err + } - m.writerKernelModules(w, kernelModuleDir, backend.InitrdModules()) + err = m.writerKernelModules(w, kernelModuleDir, backend.InitModules()) + if err != nil { + return -1, err + } - w.WriteFile("etc/systemd/system/fakemachine.service", + err = w.WriteFile("etc/systemd/system/fakemachine.service", fmt.Sprintf(serviceTemplate, backend.JobOutputTTY(), strings.Join(m.Environ, " ")), 0644) + if err != nil { + return -1, err + } - w.WriteSymlink( + err = w.WriteSymlink( "/lib/systemd/system/serial-getty@ttyS0.service", "/dev/null", 0755) + if err != nil { + return -1, err + } - w.WriteFile("/wrapper", + err = w.WriteFile("/wrapper", fmt.Sprintf(commandWrapper, backend.Name(), command), 0755) + if err != nil { + return -1, err + } - w.WriteFileRaw("/init", executeInitScriptTemplate(m, backend), 0755) + init, err := executeInitScriptTemplate(m, backend) + if err != nil { + return -1, err + } - m.generateFstab(w, backend) + err = w.WriteFileRaw("/init", init, 0755) + if err != nil { + return -1, err + } + + err = m.generateFstab(w, backend) + if err != nil { + return -1, err + } for _, v := range extracontent { - w.CopyFileTo(v[0], v[1]) + err = w.CopyFileTo(v[0], v[1]) + if err != nil { + return -1, err + } } w.Close() diff --git a/machine_test.go b/machine_test.go index 1f38ad6..804a700 100644 --- a/machine_test.go +++ b/machine_test.go @@ -10,8 +10,23 @@ import ( "testing" ) +var backendName string + +func init() { + flag.StringVar(&backendName, "backend", "auto", "Fakemachine backend to use") +} + +func CreateMachine(t *testing.T) *Machine { + machine, err := NewMachineWithBackend(backendName) + assert.Nil(t, err) + machine.SetNumCPUs(2) + + return machine +} + func TestSuccessfullCommand(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) exitcode, _ := m.Run("ls /") @@ -21,7 +36,8 @@ func TestSuccessfullCommand(t *testing.T) { } func TestCommandNotFound(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) exitcode, _ := m.Run("/a/b/c /") if exitcode != 127 { @@ -30,10 +46,12 @@ func TestCommandNotFound(t *testing.T) { } func TestImage(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) - m.CreateImage("test.img", 1024*1024) - exitcode, _ := m.Run("test -b /dev/vda") + _, err := m.CreateImage("test.img", 1024*1024) + assert.Nil(t, err) + exitcode, _ := m.Run("test -b /dev/disk/by-fakemachine-label/fakedisk-0") if exitcode != 0 { t.Fatalf("Test for the virtual image device failed with %d", exitcode) @@ -63,12 +81,13 @@ func AssertMount(t *testing.T, mountpoint, fstype string) { } func TestScratchTmp(t *testing.T) { + t.Parallel() if InMachine() { AssertMount(t, "/scratch", "tmpfs") return } - m := NewMachine() + m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchTmp"}) @@ -78,12 +97,13 @@ func TestScratchTmp(t *testing.T) { } func TestScratchDisk(t *testing.T) { + t.Parallel() if InMachine() { AssertMount(t, "/scratch", "ext4") return } - m := NewMachine() + m := CreateMachine(t) m.SetScratch(1024*1024*1024, "") exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchDisk"}) @@ -94,7 +114,8 @@ func TestScratchDisk(t *testing.T) { } func TestMemory(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) m.SetMemory(1024) // Nasty hack, this gets a chunk of shell script inserted in the wrapper script @@ -115,13 +136,13 @@ fi } func TestSpawnMachine(t *testing.T) { - + t.Parallel() if InMachine() { t.Log("Running in the machine") return } - m := NewMachine() + m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestSpawnMachine"}) @@ -131,6 +152,7 @@ func TestSpawnMachine(t *testing.T) { } func TestImageLabel(t *testing.T) { + t.Parallel() if InMachine() { t.Log("Running in the machine") devices := flag.Args() @@ -150,7 +172,7 @@ func TestImageLabel(t *testing.T) { return } - m := NewMachine() + m := CreateMachine(t) autolabel, err := m.CreateImage("test-autolabel.img", 1024*1024) assert.Nil(t, err) diff --git a/testdata/test b/testdata/test new file mode 100644 index 0000000000000000000000000000000000000000..dd33a6b19721565356053d55f7711e4927422ba8 GIT binary patch literal 16384 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 OS^>N%WT!C(j-!9Eu*}8) literal 0 HcmV?d00001 diff --git a/testdata/test.gz b/testdata/test.gz new file mode 100644 index 0000000000000000000000000000000000000000..91d59b52487283553deeac8c531935d5ad66033e GIT binary patch literal 16412 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 literal 0 HcmV?d00001 diff --git a/testdata/test.xz b/testdata/test.xz new file mode 100644 index 0000000000000000000000000000000000000000..6a17b3a5c3fa735b41660993d14f529a890e754e GIT binary patch literal 16444 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 Z7#kWGBUQSmZ9J0xn+bSE00T>86abq-;6VTY literal 0 HcmV?d00001 diff --git a/testdata/test.zst b/testdata/test.zst new file mode 100644 index 0000000000000000000000000000000000000000..5641b91682af1dabcb8dd7ac3721d67143c366ee GIT binary patch literal 16398 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

>C@vkYMDT_4;tJ#+(pX}5(szwLZ9Lo=U=>bd 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 literal 0 HcmV?d00001 From a7949c548e6aa4b6337c9783fd448e7d5a270cab Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:21:23 +0100 Subject: [PATCH 11/52] New upstream version 0.0.3 --- .github/dependabot.yml | 10 + .github/workflows/ci.yml | 71 +++++++ .golangci.yml | 4 + Jenkinsfile | 30 --- README.md | 23 +++ backend.go | 80 +++++--- backend_kvm.go => backend_qemu.go | 161 ++++++++-------- backend_uml.go | 43 ++--- bors.toml | 2 + cmd/fakemachine/main.go | 32 ++-- cpio/writerhelper.go | 186 +++++++++++++++---- decompressors.go | 48 +++++ decompressors_test.go | 89 +++++++++ go.mod | 13 ++ go.sum | 28 +++ machine.go | 296 ++++++++++++++++++++++-------- machine_test.go | 44 +++-- testdata/test | Bin 0 -> 16384 bytes testdata/test.gz | Bin 0 -> 16412 bytes testdata/test.xz | Bin 0 -> 16444 bytes testdata/test.zst | Bin 0 -> 16398 bytes 21 files changed, 858 insertions(+), 302 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .golangci.yml delete mode 100644 Jenkinsfile create mode 100644 README.md rename backend_kvm.go => backend_qemu.go (69%) create mode 100644 bors.toml create mode 100644 decompressors.go create mode 100644 decompressors_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 testdata/test create mode 100644 testdata/test.gz create mode 100644 testdata/test.xz create mode 100644 testdata/test.zst diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..615dfde --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a51287 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: Build and Test + +on: + push: + branches-ignore: + - '*.tmp' + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + + test: + strategy: + fail-fast: false + matrix: + # Currently nested virtualisation (hence kvm) is not supported on GitHub + # actions; but the qemu backend is enough to test Fakemachine + # functionality without hardware acceleration since the majority of code + # is shared between the qemu and kvm backends. + # See https://github.com/actions/runner-images/issues/183 + # + # For Arch Linux uml is not yet supported, so only test under qemu there. + os: [bullseye, bookworm] + backend: [qemu, uml] + include: + - os: arch + backend: qemu + name: Test ${{matrix.os}} with ${{matrix.backend}} backend + runs-on: ubuntu-latest + defaults: + run: + shell: bash + container: + image: ghcr.io/go-debos/test-containers/fakemachine-${{matrix.os}}:main + options: >- + --security-opt label=disable + --cap-add=SYS_PTRACE + --tmpfs /scratch:exec + env: + TMP: /scratch + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Test build + run: go build -o fakemachine cmd/fakemachine/main.go + + - name: Run unit tests (${{matrix.backend}} backend) + run: go test -v ./... --backend=${{matrix.backend}} | tee test.out + + - name: Ensure no tests were skipped + run: "! grep -q SKIP test.out" + + # Job to key the bors success status against + bors: + name: bors + if: success() + needs: + - golangci + - test + runs-on: ubuntu-latest + steps: + - name: Mark the job as a success + run: exit 0 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..72f3064 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,4 @@ +linters: + enable: + - gofmt + - whitespace diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 48b00c8..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,30 +0,0 @@ -pipeline { - agent { - dockerfile { - args '--device=/dev/kvm' - } - } - environment { - GOPATH="${env.WORKSPACE}/.gopath" - } - stages { - stage("Setup path") { - steps { - sh "mkdir -p .gopath/src/github.com/go-debos" - sh "ln -sf ${env.WORKSPACE} .gopath/src/github.com/go-debos/fakemachine" - sh "go get -v -t -d ./..." - } - } - stage("Run test") { - steps { - sh "go test -v" - } - } - - stage("Test build cmd") { - steps { - sh "go install github.com/go-debos/fakemachine/cmd/fakemachine" - } - } - } -} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fed5766 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# fakemchine - fake a machine + +Creates a vm based on the currently running system. + +## Synopsis + + fakemachine [OPTIONS] + +``` +Application Options: + -b, --backend=[auto|kvm|uml|qemu] Virtualisation backend to use (default: auto) + -v, --volume= volume to mount + -i, --image= image to add + -e, --environ-var= Environment variables (use -e VARIABLE:VALUE syntax) + -m, --memory= Amount of memory for the fakemachine in megabytes + -c, --cpus= Number of CPUs for the fakemachine + -s, --scratchsize= On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, + memory backed scratch space is used + --show-boot Show boot/console messages from the fakemachine + +Help Options: + -h, --help Show this help message +``` diff --git a/backend.go b/backend.go index 948bef6..134ca01 100644 --- a/backend.go +++ b/backend.go @@ -1,41 +1,65 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine -import( +import ( "fmt" ) -// A list of backends which are implemented +// List of backends in order of their priority in the "auto" algorithm +func implementedBackends(m *Machine) []backend { + return []backend{ + newKvmBackend(m), + newUmlBackend(m), + newQemuBackend(m), + } +} + +/* A list of backends which are implemented - sorted in order in which the + * "auto" backend chooses them. + */ func BackendNames() []string { - return []string{"auto", "kvm", "uml"} + names := []string{"auto"} + + for _, backend := range implementedBackends(nil) { + names = append(names, backend.Name()) + } + + return names } +/* The "auto" backend loops through each backend, starting with the lowest order. + * The backend is created and checked if the creation was successful (i.e. it is + * supported on this machine). If so, that backend is used for the fakemachine. If + * unsuccessful, the next backend is created until no more backends remain then + * an error is thrown explaining why each backend was unsuccessful. + */ func newBackend(name string, m *Machine) (backend, error) { + backends := implementedBackends(m) var b backend - - switch name { - case "auto": - // select kvm first - b, kvm_err := newBackend("kvm", m) - if kvm_err == nil { + var err error + + if name == "auto" { + for _, backend := range backends { + backendName := backend.Name() + b, backendErr := newBackend(backendName, m) + if backendErr != nil { + err = fmt.Errorf("%v, %v", err, backendErr) + continue + } return b, nil } + return nil, err + } - // falling back to uml - b, uml_err := newBackend("uml", m) - if uml_err == nil { - return b, nil + // find backend by name + for _, backend := range backends { + if backend.Name() == name { + b = backend } - - // no backend supported - return nil, fmt.Errorf("%v, %v", kvm_err, uml_err) - case "kvm": - b = newKvmBackend(m) - case "uml": - b = newUmlBackend(m) - default: + } + if b == nil { return nil, fmt.Errorf("%s backend does not exist", name) } @@ -58,11 +82,11 @@ type backend interface { // Get kernel release version KernelRelease() (string, error) - // The path to the kernel and modules - KernelPath() (kernelPath string, moddir string, err error) + // The path to the kernel + KernelPath() (kernelPath string, err error) - // A list of modules to include in the initrd - InitrdModules() []string + // The path to the modules + ModulePath() (moddir string, err error) // A list of udev rules UdevRules() []string @@ -76,7 +100,7 @@ type backend interface { // The parameters used to mount a specific volume into the machine MountParameters(mount mountPoint) (fstype string, options []string) - // A list of modules which should be probed in the initscript + // A list of modules to be added to initrd and probed in the initscript InitModules() []string // A list of additional volumes which should mounted in the initscript diff --git a/backend_kvm.go b/backend_qemu.go similarity index 69% rename from backend_kvm.go rename to backend_qemu.go index eacdc0c..423e1a3 100644 --- a/backend_kvm.go +++ b/backend_qemu.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -10,42 +10,36 @@ import ( "os" "os/exec" "path" - "path/filepath" "strings" "golang.org/x/sys/unix" ) -type kvmBackend struct { +type qemuBackend struct { machine *Machine } -func newKvmBackend(m *Machine) kvmBackend { - return kvmBackend{machine: m} +func newQemuBackend(m *Machine) qemuBackend { + return qemuBackend{machine: m} } -func (b kvmBackend) Name() string { - return "kvm" +func (b qemuBackend) Name() string { + return "qemu" } -func (b kvmBackend) Supported() (bool, error) { - kvmDevice, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0); - if err != nil { - return false, err - } - kvmDevice.Close() - +func (b qemuBackend) Supported() (bool, error) { if _, err := b.QemuPath(); err != nil { return false, err } + return true, nil } -func (b kvmBackend) QemuPath() (string, error) { +func (b qemuBackend) QemuPath() (string, error) { return exec.LookPath("qemu-system-x86_64") } -func (b kvmBackend) KernelRelease() (string, error) { +func (b qemuBackend) KernelRelease() (string, error) { /* First try the kernel the current system is running, but if there are no * modules for that try the latest from /lib/modules. The former works best * for systems directly running fakemachine, the latter makes sense in docker @@ -65,7 +59,7 @@ func (b kvmBackend) KernelRelease() (string, error) { return "", err } - for i := len(files)-1; i >= 0; i-- { + for i := len(files) - 1; i >= 0; i-- { /* Ensure the kernel name starts with a digit, in order * to filter out 'extramodules-ARCH' on ArchLinux */ filename := files[i].Name() @@ -77,61 +71,38 @@ func (b kvmBackend) KernelRelease() (string, error) { return "", fmt.Errorf("No kernel found") } -func (b kvmBackend) hostKernelPath(kernelRelease string) (string, error) { - kernelDir := "/boot" - kernelPrefix := "vmlinuz-" - - /* First, try to find a kernel with a well-known name */ - kernelPath := filepath.Join(kernelDir, kernelPrefix + kernelRelease) - if _, err := os.Stat(kernelPath); err == nil { - return kernelPath, nil - } - - /* Otherwise, inspect each kernel installed, and look for the release - * string straight in the binary. Not pretty, but it works. */ - needle := kernelRelease - if !strings.HasSuffix(needle, " ") { - // Add space to match exact kernel description string e.g - // 4.19.0-6-amd64 (debian-kernel@lists.debian.org) #1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11) - needle += " " +func (b qemuBackend) KernelPath() (string, error) { + /* First we look within the modules directory, as supported by + * various distributions - Arch, Fedora... + * + * ... perhaps because systemd requires it to allow hibernation + * https://github.com/systemd/systemd/commit/edda44605f06a41fb86b7ab8128dcf99161d2344 + */ + if moddir, err := b.ModulePath(); err == nil { + kernelPath := path.Join(moddir, "vmlinuz") + if _, err := os.Stat(kernelPath); err == nil { + return kernelPath, nil + } } - files, err := ioutil.ReadDir(kernelDir) + /* Fall-back to the previous method and look in /boot */ + kernelRelease, err := b.KernelRelease() if err != nil { return "", err } - for _, f := range files { - if !strings.HasPrefix(f.Name(), kernelPrefix) || f.IsDir() { - continue - } - - kernelPath := filepath.Join(kernelDir, f.Name()) - buf, err := ioutil.ReadFile(kernelPath) - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to read kernel:", err) - continue - } - - if !bytes.Contains(buf, []byte(needle)) { - continue - } - - return kernelPath, nil + kernelPath := "/boot/vmlinuz-" + kernelRelease + if _, err := os.Stat(kernelPath); err != nil { + return "", err } - return "", fmt.Errorf("No kernel found for release %s", kernelRelease) + return kernelPath, nil } -func (b kvmBackend) KernelPath() (string, string, error) { +func (b qemuBackend) ModulePath() (string, error) { kernelRelease, err := b.KernelRelease() if err != nil { - return "", "", err - } - - kernelPath, err := b.hostKernelPath(kernelRelease) - if err != nil { - return "", "", err + return "", err } moddir := "/lib/modules" @@ -141,22 +112,13 @@ func (b kvmBackend) KernelPath() (string, string, error) { moddir = path.Join(moddir, kernelRelease) if _, err := os.Stat(moddir); err != nil { - return "", "", err + return "", err } - return kernelPath, moddir, nil -} - -func (b kvmBackend) InitrdModules() []string { - return []string{"virtio_console", - "virtio", - "virtio_pci", - "virtio_ring", - "9p", - "9pnet_virtio"} + return moddir, nil } -func (b kvmBackend) UdevRules() []string { +func (b qemuBackend) UdevRules() []string { udevRules := []string{} // create symlink under /dev/disk/by-fakemachine-label/ for each virtual image @@ -169,11 +131,11 @@ func (b kvmBackend) UdevRules() []string { return udevRules } -func (b kvmBackend) NetworkdMatch() string { +func (b qemuBackend) NetworkdMatch() string { return "e*" } -func (b kvmBackend) JobOutputTTY() string { +func (b qemuBackend) JobOutputTTY() string { // By default we send job output to the second virtio console, // reserving /dev/ttyS0 for boot messages (which we ignore) // and /dev/hvc0 for possible use by systemd as a getty @@ -186,36 +148,45 @@ func (b kvmBackend) JobOutputTTY() string { return "/dev/hvc0" } -func (b kvmBackend) MountParameters(mount mountPoint) (string, []string) { +func (b qemuBackend) MountParameters(mount mountPoint) (string, []string) { return "9p", []string{"trans=virtio", "version=9p2000.L", "cache=loose", "msize=262144"} } -func (b kvmBackend) InitModules() []string { +func (b qemuBackend) InitModules() []string { return []string{"virtio_pci", "virtio_console", "9pnet_virtio", "9p"} } -func (b kvmBackend) InitStaticVolumes() []mountPoint { +func (b qemuBackend) InitStaticVolumes() []mountPoint { return []mountPoint{} } -func (b kvmBackend) Start() (bool, error) { +func (b qemuBackend) Start() (bool, error) { + return b.StartQemu(false) +} + +func (b qemuBackend) StartQemu(kvm bool) (bool, error) { m := b.machine - kernelPath, _, err := b.KernelPath() + kernelPath, err := b.KernelPath() if err != nil { return false, err } memory := fmt.Sprintf("%d", m.memory) numcpus := fmt.Sprintf("%d", m.numcpus) qemuargs := []string{"qemu-system-x86_64", - "-cpu", "host", "-smp", numcpus, "-m", memory, - "-enable-kvm", "-kernel", kernelPath, "-initrd", m.initrdpath, "-display", "none", "-no-reboot"} + + if kvm { + qemuargs = append(qemuargs, + "-cpu", "host", + "-enable-kvm") + } + kernelargs := []string{"console=ttyS0", "panic=-1", "systemd.unit=fakemachine.service"} @@ -281,3 +252,29 @@ func (b kvmBackend) Start() (bool, error) { return pstate.Success(), nil } + +type kvmBackend struct { + qemuBackend +} + +func newKvmBackend(m *Machine) kvmBackend { + return kvmBackend{qemuBackend{machine: m}} +} + +func (b kvmBackend) Name() string { + return "kvm" +} + +func (b kvmBackend) Supported() (bool, error) { + kvmDevice, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0) + if err != nil { + return false, err + } + kvmDevice.Close() + + return b.qemuBackend.Supported() +} + +func (b kvmBackend) Start() (bool, error) { + return b.StartQemu(true) +} diff --git a/backend_uml.go b/backend_uml.go index ef398d9..3f0bc6e 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -28,8 +28,12 @@ func (b umlBackend) Name() string { func (b umlBackend) Supported() (bool, error) { // check the kernel exists - _, _, err := b.KernelPath() - if err != nil { + if _, err := b.KernelPath(); err != nil { + return false, err + } + + // check the modules exist + if _, err := b.ModulePath(); err != nil { return false, err } @@ -44,41 +48,40 @@ func (b umlBackend) KernelRelease() (string, error) { return "", errors.New("Not implemented") } -func (b umlBackend) KernelPath() (string, string, error) { +func (b umlBackend) KernelPath() (string, error) { // find the UML binary kernelPath, err := exec.LookPath("linux.uml") if err != nil { - return "", "", fmt.Errorf("user-mode-linux not installed") + return "", fmt.Errorf("user-mode-linux not installed") } + return kernelPath, nil +} +func (b umlBackend) ModulePath() (string, error) { // make sure the UML modules exist // on non-merged usr systems the modules still reside under /usr/lib/uml moddir := "/usr/lib/uml/modules" if _, err := os.Stat(moddir); err != nil { - return "", "", fmt.Errorf("user-mode-linux modules not installed") + return "", fmt.Errorf("user-mode-linux modules not installed") } // find the subdirectory containing the modules for the UML release modSubdirs, err := ioutil.ReadDir(moddir) if err != nil { - return "", "", err + return "", err } if len(modSubdirs) != 1 { - return "", "", fmt.Errorf("could not determine which user-mode-linux modules to use") + return "", fmt.Errorf("could not determine which user-mode-linux modules to use") } moddir = path.Join(moddir, modSubdirs[0].Name()) - return kernelPath, moddir, nil + return moddir, nil } func (b umlBackend) SlirpHelperPath() (string, error) { return exec.LookPath("libslirp-helper") } -func (b umlBackend) InitrdModules() []string { - return []string{} -} - func (b umlBackend) UdevRules() []string { udevRules := []string{} @@ -117,7 +120,7 @@ func (b umlBackend) InitModules() []string { func (b umlBackend) InitStaticVolumes() []mountPoint { // mount the UML modules over the top of /lib/modules // which currently contains the modules from the base system - _, moddir, _ := b.KernelPath() + moddir, _ := b.ModulePath() moddir = path.Join(moddir, "../") machineDir := "/lib/modules" @@ -132,7 +135,7 @@ func (b umlBackend) InitStaticVolumes() []mountPoint { func (b umlBackend) Start() (bool, error) { m := b.machine - kernelPath, _, err := b.KernelPath() + kernelPath, err := b.KernelPath() if err != nil { return false, err } @@ -170,10 +173,9 @@ func (b umlBackend) Start() (bool, error) { } defer umlVectorTransportSocket.Close() - // launch libslirp-helper slirpHelperArgs := []string{"libslirp-helper", - "--exit-with-parent"} + "--exit-with-parent"} /* attach the slirpHelperSocket as an additional fd to the process, * after std*. The helper then bridges the host network to the attached @@ -190,8 +192,7 @@ func (b umlBackend) Start() (bool, error) { if err != nil { return false, err } - defer slirpHelper.Kill() - + defer func() { _ = slirpHelper.Kill() }() // launch uml guest memory := fmt.Sprintf("%d", m.memory) @@ -229,7 +230,7 @@ func (b umlBackend) Start() (bool, error) { umlargs = append(umlargs, "con1=fd:0,fd:1", "con0=null", - "con=none") // no other consoles + "con=none") // no other consoles } for i, img := range m.images { diff --git a/bors.toml b/bors.toml new file mode 100644 index 0000000..1db5825 --- /dev/null +++ b/bors.toml @@ -0,0 +1,2 @@ +status = [ "bors" ] +delete_merged_branches = true diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index d9598b6..f2865c0 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -10,14 +10,14 @@ import ( ) type Options struct { - Backend string `short:"b" long:"backend" description:"Virtualisation backend to use" default:"auto"` - Volumes []string `short:"v" long:"volume" description:"volume to mount"` - Images []string `short:"i" long:"image" description:"image to add"` + Backend string `short:"b" long:"backend" description:"Virtualisation backend to use" default:"auto"` + Volumes []string `short:"v" long:"volume" description:"volume to mount"` + Images []string `short:"i" long:"image" description:"image to add"` EnvironVars map[string]string `short:"e" long:"environ-var" description:"Environment variables (use -e VARIABLE:VALUE syntax)"` - Memory int `short:"m" long:"memory" description:"Amount of memory for the fakemachine in megabytes"` - CPUs int `short:"c" long:"cpus" description:"Number of CPUs for the fakemachine"` - ScratchSize string `short:"s" long:"scratchsize" description:"On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, memory backed scratch space is used"` - ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fakemachine"` + Memory int `short:"m" long:"memory" description:"Amount of memory for the fakemachine in megabytes"` + CPUs int `short:"c" long:"cpus" description:"Number of CPUs for the fakemachine"` + ScratchSize string `short:"s" long:"scratchsize" description:"On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, memory backed scratch space is used"` + ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fakemachine"` } var options Options @@ -29,8 +29,8 @@ func warnLocalhost(variable string, value string) { Consider using an address that is valid on your network.` if strings.Contains(value, "localhost") || - strings.Contains(value, "127.0.0.1") || - strings.Contains(value, "::1") { + strings.Contains(value, "127.0.0.1") || + strings.Contains(value, "::1") { fmt.Printf(message, variable) } } @@ -88,7 +88,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { // These are the environment variables that will be detected on the // host and propagated to fakemachine. These are listed lower case, but // they are detected and configured in both lower case and upper case. - var environ_vars = [...]string { + var environ_vars = [...]string{ "http_proxy", "https_proxy", "ftp_proxy", @@ -123,14 +123,12 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { } // Puts in a format that is compatible with output of os.Environ() - if EnvironVars != nil { - EnvironString := []string{} - for k, v := range EnvironVars { - warnLocalhost(k, v) - EnvironString = append(EnvironString, fmt.Sprintf("%s=%s", k, v)) - } - m.SetEnviron(EnvironString) // And save the resulting environ vars on m + EnvironString := []string{} + for k, v := range EnvironVars { + warnLocalhost(k, v) + EnvironString = append(EnvironString, fmt.Sprintf("%s=%s", k, v)) } + m.SetEnviron(EnvironString) // And save the resulting environ vars on m } func main() { diff --git a/cpio/writerhelper.go b/cpio/writerhelper.go index f8a69dc..72cce4d 100644 --- a/cpio/writerhelper.go +++ b/cpio/writerhelper.go @@ -1,8 +1,9 @@ package writerhelper import ( + "bytes" + "fmt" "io" - "log" "os" "path" "path/filepath" @@ -16,6 +17,19 @@ type WriterHelper struct { *cpio.Writer } +type WriteDirectory struct { + Directory string + Perm os.FileMode +} + +type WriteSymlink struct { + Target string + Link string + Perm os.FileMode +} + +type Transformer func(dst io.Writer, src io.Reader) error + func NewWriterHelper(f io.Writer) *WriterHelper { return &WriterHelper{ paths: map[string]bool{"/": true}, @@ -23,11 +37,11 @@ func NewWriterHelper(f io.Writer) *WriterHelper { } } -func (w *WriterHelper) ensureBaseDirectory(directory string) { +func (w *WriterHelper) ensureBaseDirectory(directory string) error { d := path.Clean(directory) if w.paths[d] { - return + return nil } components := strings.Split(directory, "/") @@ -39,12 +53,30 @@ func (w *WriterHelper) ensureBaseDirectory(directory string) { continue } - w.WriteDirectory(collector, 0755) + err := w.WriteDirectory(collector, 0755) + if err != nil { + return err + } + } + + return nil +} + +func (w *WriterHelper) WriteDirectories(directories []WriteDirectory) error { + for _, d := range directories { + err := w.WriteDirectory(d.Directory, d.Perm) + if err != nil { + return err + } } + return nil } -func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(directory)) +func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(directory)) + if err != nil { + return err + } hdr := new(cpio.Header) @@ -52,17 +84,24 @@ func (w *WriterHelper) WriteDirectory(directory string, perm os.FileMode) { hdr.Name = directory hdr.Mode = int64(perm) - w.WriteHeader(hdr) + err = w.WriteHeader(hdr) + if err != nil { + return err + } w.paths[directory] = true + return nil } -func (w *WriterHelper) WriteFile(file, content string, perm os.FileMode) { - w.WriteFileRaw(file, []byte(content), perm) +func (w *WriterHelper) WriteFile(file, content string, perm os.FileMode) error { + return w.WriteFileRaw(file, []byte(content), perm) } -func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(file)) +func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(file)) + if err != nil { + return err + } hdr := new(cpio.Header) @@ -71,12 +110,30 @@ func (w *WriterHelper) WriteFileRaw(file string, bytes []byte, perm os.FileMode) hdr.Mode = int64(perm) hdr.Size = int64(len(bytes)) - w.WriteHeader(hdr) - w.Write(bytes) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + _, err = w.Write(bytes) + return err +} + +func (w *WriterHelper) WriteSymlinks(links []WriteSymlink) error { + for _, l := range links { + err := w.WriteSymlink(l.Target, l.Link, l.Perm) + if err != nil { + return err + } + } + return nil } -func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(link)) +func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(link)) + if err != nil { + return err + } + hdr := new(cpio.Header) content := []byte(target) @@ -86,13 +143,20 @@ func (w *WriterHelper) WriteSymlink(target, link string, perm os.FileMode) { hdr.Mode = int64(perm) hdr.Size = int64(len(content)) - w.WriteHeader(hdr) - w.Write(content) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = w.Write(content) + return err } -func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, - perm os.FileMode) { - w.ensureBaseDirectory(path.Dir(device)) +func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, perm os.FileMode) error { + err := w.ensureBaseDirectory(path.Dir(device)) + if err != nil { + return err + } hdr := new(cpio.Header) hdr.Type = cpio.TYPE_CHAR @@ -101,32 +165,39 @@ func (w *WriterHelper) WriteCharDevice(device string, major, minor int64, hdr.Devmajor = major hdr.Devminor = minor - w.WriteHeader(hdr) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + return nil } -func (w *WriterHelper) CopyTree(path string) { - walker := func(p string, info os.FileInfo, err error) error { +func (w *WriterHelper) CopyTree(path string) error { + walker := func(p string, info os.FileInfo, _ error) error { + var err error if info.Mode().IsDir() { - w.WriteDirectory(p, info.Mode() & ^os.ModeType) + err = w.WriteDirectory(p, info.Mode() & ^os.ModeType) } else if info.Mode().IsRegular() { - w.CopyFile(p) + err = w.CopyFile(p) } else { - panic("No handled") + err = fmt.Errorf("file type not handled for %s", p) } - return nil + return err } - filepath.Walk(path, walker) + return filepath.Walk(path, walker) } func (w *WriterHelper) CopyFileTo(src, dst string) error { - w.ensureBaseDirectory(path.Dir(dst)) + err := w.ensureBaseDirectory(path.Dir(dst)) + if err != nil { + return err + } f, err := os.Open(src) if err != nil { - log.Panicf("open failed: %s - %v", src, err) - return err + return fmt.Errorf("open failed: %s - %v", src, err) } defer f.Close() @@ -142,8 +213,57 @@ func (w *WriterHelper) CopyFileTo(src, dst string) error { hdr.Mode = int64(info.Mode() & ^os.ModeType) hdr.Size = info.Size() - w.WriteHeader(hdr) - io.Copy(w, f) + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = io.Copy(w, f) + if err != nil { + return err + } + + return nil +} + +func (w *WriterHelper) TransformFileTo(src, dst string, fn Transformer) error { + err := w.ensureBaseDirectory(path.Dir(dst)) + if err != nil { + return err + } + + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return err + } + + out := new(bytes.Buffer) + err = fn(out, f) + if err != nil { + return err + } + + hdr := new(cpio.Header) + hdr.Type = cpio.TYPE_REG + hdr.Name = dst + hdr.Mode = int64(info.Mode() & ^os.ModeType) + hdr.Size = int64(out.Len()) + + err = w.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = io.Copy(w, out) + if err != nil { + return err + } return nil } diff --git a/decompressors.go b/decompressors.go new file mode 100644 index 0000000..05edcc2 --- /dev/null +++ b/decompressors.go @@ -0,0 +1,48 @@ +package fakemachine + +import ( + "compress/gzip" + "io" + + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +func ZstdDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := zstd.NewReader(src) + if err != nil { + return err + } + defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func XzDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := xz.NewReader(src) + if err != nil { + return err + } + // There is no Close() API. See: https://github.com/ulikunitz/xz/issues/45 + //defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func GzipDecompressor(dst io.Writer, src io.Reader) error { + decompressor, err := gzip.NewReader(src) + if err != nil { + return err + } + defer decompressor.Close() + + _, err = io.Copy(dst, decompressor) + return err +} + +func NullDecompressor(dst io.Writer, src io.Reader) error { + _, err := io.Copy(dst, src) + return err +} diff --git a/decompressors_test.go b/decompressors_test.go new file mode 100644 index 0000000..8b1a77e --- /dev/null +++ b/decompressors_test.go @@ -0,0 +1,89 @@ +package fakemachine + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "path" + "testing" + + "github.com/go-debos/fakemachine/cpio" +) + +func checkStreamsMatch(t *testing.T, output, check io.Reader) error { + i := 0 + oreader := bufio.NewReader(output) + creader := bufio.NewReader(check) + for { + ochar, oerr := oreader.ReadByte() + cchar, cerr := creader.ReadByte() + if oerr != nil || cerr != nil { + if oerr == io.EOF && cerr == io.EOF { + return nil + } + if oerr != nil && oerr != io.EOF { + t.Errorf("Error reading output stream: %s", oerr) + return oerr + } + if cerr != nil && cerr != io.EOF { + t.Errorf("Error reading check stream: %s", cerr) + return cerr + } + return nil + } + + if ochar != cchar { + t.Errorf("Mismatch at byte %d, values %d (output) and %d (check)", + i, ochar, cchar) + return errors.New("Data mismatch") + } + i += 1 + } +} + +func decompressorTest(t *testing.T, file, suffix string, d writerhelper.Transformer) { + f, err := os.Open(path.Join("testdata", file+suffix)) + if err != nil { + t.Errorf("Unable to open test data: %s", err) + return + } + defer f.Close() + + output := new(bytes.Buffer) + err = d(output, f) + if err != nil { + t.Errorf("Error whilst decompressing test file: %s", err) + return + } + + check_f, err := os.Open(path.Join("testdata", file)) + if err != nil { + t.Errorf("Unable to open check data: %s", err) + return + } + defer check_f.Close() + + err = checkStreamsMatch(t, output, check_f) + if err != nil { + t.Errorf("Failed to compare streams: %s", err) + return + } +} + +func TestZstd(t *testing.T) { + decompressorTest(t, "test", ".zst", ZstdDecompressor) +} + +func TestXz(t *testing.T) { + decompressorTest(t, "test", ".xz", XzDecompressor) +} + +func TestGzip(t *testing.T) { + decompressorTest(t, "test", ".gz", GzipDecompressor) +} + +func TestNull(t *testing.T) { + decompressorTest(t, "test", "", NullDecompressor) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d0daba --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/go-debos/fakemachine + +go 1.15 + +require ( + github.com/docker/go-units v0.5.0 + github.com/jessevdk/go-flags v1.5.0 + github.com/klauspost/compress v1.15.3 + github.com/stretchr/testify v1.8.0 + github.com/surma/gocpio v1.1.0 + github.com/ulikunitz/xz v0.5.10 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b1d9995 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/klauspost/compress v1.15.3 h1:wmfu2iqj9q22SyMINp1uQ8C2/V4M1phJdmH9fG4nba0= +github.com/klauspost/compress v1.15.3/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/surma/gocpio v1.1.0 h1:RUWT+VqJ8GSodSv7Oh5xjIxy7r24CV1YvothHFfPxcQ= +github.com/surma/gocpio v1.1.0/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9LCVI= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/machine.go b/machine.go index f1a0189..ae23a67 100644 --- a/machine.go +++ b/machine.go @@ -1,5 +1,5 @@ -// +build linux -// +build amd64 +//go:build linux && amd64 +// +build linux,amd64 package fakemachine @@ -24,11 +24,7 @@ import ( func mergedUsrSystem() bool { f, _ := os.Lstat("/bin") - if (f.Mode() & os.ModeSymlink) == os.ModeSymlink { - return true - } - - return false + return (f.Mode() & os.ModeSymlink) == os.ModeSymlink } // Parse modinfo output and return the value of module attributes @@ -55,7 +51,7 @@ func getModData(modname string, fieldname string, kernelRelease string) []string // Get full path of module func getModPath(modname string, kernelRelease string) string { path := getModData(modname, "filename", kernelRelease) - if len(path) != 0 { + if len(path) != 0 { return path[0] } return "" @@ -66,7 +62,7 @@ func getModDepends(modname string, kernelRelease string) []string { deplist := getModData(modname, "depends", kernelRelease) var modlist []string for _, v := range deplist { - if v != "" { + if v != "" { modlist = append(modlist, strings.Split(v, ",")...) } } @@ -74,6 +70,13 @@ func getModDepends(modname string, kernelRelease string) []string { return modlist } +var suffixes = map[string]writerhelper.Transformer{ + ".ko": NullDecompressor, + ".ko.gz": GzipDecompressor, + ".ko.xz": XzDecompressor, + ".ko.zst": ZstdDecompressor, +} + func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copiedModules map[string]bool) error { release, _ := m.backend.KernelRelease() modpath := getModPath(modname, release) @@ -90,11 +93,30 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi prefix = "/usr" } - if err := w.CopyFile(prefix + modpath); err != nil { - return err + found := false + for suffix, fn := range suffixes { + if strings.HasSuffix(modpath, suffix) { + // File must exist as-is on the filesystem. Aka do not + // fallback to other suffixes. + if _, err := os.Stat(modpath); err != nil { + return err + } + + // The suffix is the complete thing - ".ko.foobar" + // Reinstate the required ".ko" part, after trimming. + basepath := strings.TrimSuffix(modpath, suffix) + ".ko" + if err := w.TransformFileTo(modpath, prefix+basepath, fn); err != nil { + return err + } + found = true + break + } + } + if !found { + return errors.New("Module extension/suffix unknown") } - copiedModules[modname] = true; + copiedModules[modname] = true deplist := getModDepends(modname, release) for _, mod := range deplist { @@ -150,12 +172,8 @@ type Machine struct { } // Create a new machine object with the auto backend -func NewMachine() *Machine { - m, err := NewMachineWithBackend("auto") - if err != nil { - panic(err) - } - return m +func NewMachine() (*Machine, error) { + return NewMachineWithBackend("auto") } // Create a new machine object @@ -314,9 +332,9 @@ func tmplStaticVolumes(m Machine) []mountPoint { return mounts } -func executeInitScriptTemplate(m *Machine, b backend) []byte { +func executeInitScriptTemplate(m *Machine, b backend) ([]byte, error) { helperFuncs := template.FuncMap{ - "MountVolume": tmplMountVolume, + "MountVolume": tmplMountVolume, "StaticVolumes": tmplStaticVolumes, } @@ -329,9 +347,9 @@ func executeInitScriptTemplate(m *Machine, b backend) []byte { tmpl := template.Must(template.New("init").Funcs(helperFuncs).Parse(initScript)) out := &bytes.Buffer{} if err := tmpl.Execute(out, tmplVariables); err != nil { - panic(err) + return nil, err } - return out.Bytes() + return out.Bytes(), nil } func (m *Machine) addStaticVolume(directory, label string) { @@ -440,7 +458,7 @@ func (m *Machine) SetScratch(scratchsize int64, path string) { } } -func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) { +func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) error { fstab := []string{"# Generated fstab file by fakemachine"} if m.scratchfile == "" { @@ -458,30 +476,54 @@ func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) { } fstab = append(fstab, "") - w.WriteFile("/etc/fstab", strings.Join(fstab, "\n"), 0755) + err := w.WriteFile("/etc/fstab", strings.Join(fstab, "\n"), 0755) + return err +} + +func stripCompressionSuffix(module string) (string, error) { + for suffix := range suffixes { + if strings.HasSuffix(module, suffix) { + // The suffix is the complete thing - ".ko.foobar" + // Reinstate the required ".ko" part, after trimming. + return strings.TrimSuffix(module, suffix) + ".ko", nil + } + } + return "", errors.New("Module extension/suffix unknown") +} + +func (m *Machine) generateModulesDep(w *writerhelper.WriterHelper, moddir string, modules map[string]bool) error { + output := make([]string, len(modules)) + release, _ := m.backend.KernelRelease() + i := 0 + for mod := range modules { + modpath, _ := stripCompressionSuffix(getModPath(mod, release)) // CANNOT fail + deplist := getModDepends(mod, release) // CANNOT fail + deps := make([]string, len(deplist)) + for j, dep := range deplist { + deppath, _ := stripCompressionSuffix(getModPath(dep, release)) // CANNOT fail + deps[j] = deppath + } + output[i] = fmt.Sprintf("%s: %s", modpath, strings.Join(deps, " ")) + i += 1 + } + + path := path.Join(moddir, "modules.dep") + return w.WriteFile(path, strings.Join(output, "\n"), 0644) } func (m *Machine) SetEnviron(environ []string) { m.Environ = environ } - func (m *Machine) writerKernelModules(w *writerhelper.WriterHelper, moddir string, modules []string) error { if len(modules) == 0 { return nil } - modfiles := []string {"modules.order", - "modules.builtin", - "modules.dep", - "modules.dep.bin", - "modules.alias", - "modules.alias.bin", - "modules.softdep", - "modules.symbols", - "modules.symbols.bin", - "modules.builtin.bin", - "modules.devname"} + modfiles := []string{ + "modules.builtin", + "modules.alias", + "modules.symbols"} for _, v := range modfiles { if err := w.CopyFile(moddir + "/" + v); err != nil { @@ -491,12 +533,13 @@ func (m *Machine) writerKernelModules(w *writerhelper.WriterHelper, moddir strin copiedModules := make(map[string]bool) - for _, modname := range modules { + for _, modname := range modules { if err := m.copyModules(w, modname, copiedModules); err != nil { return err } } - return nil + + return m.generateModulesDep(w, moddir, copiedModules) } func (m *Machine) setupscratch() error { @@ -533,7 +576,7 @@ func (m *Machine) cleanup() { func (m *Machine) startup(command string, extracontent [][2]string) (int, error) { defer m.cleanup() - os.Setenv("PATH", os.Getenv("PATH") + ":/sbin:/usr/sbin") + os.Setenv("PATH", os.Getenv("PATH")+":/sbin:/usr/sbin") tmpdir, err := ioutil.TempDir("", "fakemachine-") if err != nil { @@ -556,35 +599,52 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) backend := m.backend - _, kernelModuleDir, err := backend.KernelPath() + kernelModuleDir, err := backend.ModulePath() if err != nil { return -1, err } w := writerhelper.NewWriterHelper(f) - w.WriteDirectory("/scratch", 01777) - w.WriteDirectory("/var/tmp", 01777) - w.WriteDirectory("/var/lib/dbus", 0755) - - w.WriteDirectory("/tmp", 01777) - w.WriteDirectory("/sys", 0755) - w.WriteDirectory("/proc", 0755) - w.WriteDirectory("/run", 0755) - w.WriteDirectory("/usr", 0755) - w.WriteDirectory("/usr/bin", 0755) - w.WriteDirectory("/lib64", 0755) + err = w.WriteDirectories([]writerhelper.WriteDirectory{ + {Directory: "/scratch", Perm: 01777}, + {Directory: "/var/tmp", Perm: 01777}, + {Directory: "/var/lib/dbus", Perm: 0755}, + {Directory: "/tmp", Perm: 01777}, + {Directory: "/sys", Perm: 0755}, + {Directory: "/proc", Perm: 0755}, + {Directory: "/run", Perm: 0755}, + {Directory: "/usr", Perm: 0755}, + {Directory: "/usr/bin", Perm: 0755}, + {Directory: "/lib64", Perm: 0755}, + }) + if err != nil { + return -1, err + } - w.WriteSymlink("/run", "/var/run", 0755) + err = w.WriteSymlink("/run", "/var/run", 0755) + if err != nil { + return -1, err + } if mergedUsrSystem() { - w.WriteSymlink("/usr/sbin", "/sbin", 0755) - w.WriteSymlink("/usr/bin", "/bin", 0755) - w.WriteSymlink("/usr/lib", "/lib", 0755) + err = w.WriteSymlinks([]writerhelper.WriteSymlink{ + {Target: "/usr/sbin", Link: "/sbin", Perm: 0755}, + {Target: "/usr/bin", Link: "/bin", Perm: 0755}, + {Target: "/usr/lib", Link: "/lib", Perm: 0755}, + }) + if err != nil { + return -1, err + } } else { - w.WriteDirectory("/sbin", 0755) - w.WriteDirectory("/bin", 0755) - w.WriteDirectory("/lib", 0755) + err = w.WriteDirectories([]writerhelper.WriteDirectory{ + {Directory: "/sbin", Perm: 0744}, + {Directory: "/bin", Perm: 0755}, + {Directory: "/lib", Perm: 0755}, + }) + if err != nil { + return -1, err + } } prefix := "" @@ -597,63 +657,139 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) if err != nil { return -1, err } - w.CopyFileTo(busybox, prefix + "/bin/busybox") + err = w.CopyFileTo(busybox, prefix+"/bin/busybox") + if err != nil { + return -1, err + } /* Amd64 dynamic linker */ - w.CopyFile("/lib64/ld-linux-x86-64.so.2") + err = w.CopyFile("/lib64/ld-linux-x86-64.so.2") + if err != nil { + return -1, err + } /* C libraries */ libraryDir, err := realDir("/lib64/ld-linux-x86-64.so.2") if err != nil { return -1, err } - w.CopyFile(libraryDir + "/libc.so.6") - w.CopyFile(libraryDir + "/libresolv.so.2") + err = w.CopyFile(libraryDir + "/libc.so.6") + if err != nil { + return -1, err + } + err = w.CopyFile(libraryDir + "/libresolv.so.2") + if err != nil { + return -1, err + } - w.WriteCharDevice("/dev/console", 5, 1, 0700) + err = w.WriteCharDevice("/dev/console", 5, 1, 0700) + if err != nil { + return -1, err + } // Linker configuration - w.CopyFile("/etc/ld.so.conf") - w.CopyTree("/etc/ld.so.conf.d") + err = w.CopyFile("/etc/ld.so.conf") + if err != nil { + return -1, err + } + + err = w.CopyTree("/etc/ld.so.conf.d") + if err != nil { + return -1, err + } // Core system configuration - w.WriteFile("/etc/machine-id", "", 0444) - w.WriteFile("/etc/hostname", "fakemachine", 0444) + err = w.WriteFile("/etc/machine-id", "", 0444) + if err != nil { + return -1, err + } + + err = w.WriteFile("/etc/hostname", "fakemachine", 0444) + if err != nil { + return -1, err + } - w.CopyFile("/etc/passwd") - w.CopyFile("/etc/group") - w.CopyFile("/etc/nsswitch.conf") + err = w.CopyFile("/etc/passwd") + if err != nil { + return -1, err + } + + err = w.CopyFile("/etc/group") + if err != nil { + return -1, err + } + + err = w.CopyFile("/etc/nsswitch.conf") + if err != nil { + return -1, err + } // udev rules udevRules := strings.Join(backend.UdevRules(), "\n") + "\n" - w.WriteFile("/etc/udev/rules.d/61-fakemachine.rules", udevRules, 0444) + err = w.WriteFile("/etc/udev/rules.d/61-fakemachine.rules", udevRules, 0444) + if err != nil { + return -1, err + } - w.WriteFile("/etc/systemd/network/ethernet.network", + err = w.WriteFile("/etc/systemd/network/ethernet.network", fmt.Sprintf(networkdTemplate, backend.NetworkdMatch()), 0444) - w.WriteSymlink( + if err != nil { + return -1, err + } + + err = w.WriteSymlink( "/lib/systemd/resolv.conf", "/etc/resolv.conf", 0755) + if err != nil { + return -1, err + } - m.writerKernelModules(w, kernelModuleDir, backend.InitrdModules()) + err = m.writerKernelModules(w, kernelModuleDir, backend.InitModules()) + if err != nil { + return -1, err + } - w.WriteFile("etc/systemd/system/fakemachine.service", + err = w.WriteFile("etc/systemd/system/fakemachine.service", fmt.Sprintf(serviceTemplate, backend.JobOutputTTY(), strings.Join(m.Environ, " ")), 0644) + if err != nil { + return -1, err + } - w.WriteSymlink( + err = w.WriteSymlink( "/lib/systemd/system/serial-getty@ttyS0.service", "/dev/null", 0755) + if err != nil { + return -1, err + } - w.WriteFile("/wrapper", + err = w.WriteFile("/wrapper", fmt.Sprintf(commandWrapper, backend.Name(), command), 0755) + if err != nil { + return -1, err + } - w.WriteFileRaw("/init", executeInitScriptTemplate(m, backend), 0755) + init, err := executeInitScriptTemplate(m, backend) + if err != nil { + return -1, err + } - m.generateFstab(w, backend) + err = w.WriteFileRaw("/init", init, 0755) + if err != nil { + return -1, err + } + + err = m.generateFstab(w, backend) + if err != nil { + return -1, err + } for _, v := range extracontent { - w.CopyFileTo(v[0], v[1]) + err = w.CopyFileTo(v[0], v[1]) + if err != nil { + return -1, err + } } w.Close() diff --git a/machine_test.go b/machine_test.go index 1f38ad6..804a700 100644 --- a/machine_test.go +++ b/machine_test.go @@ -10,8 +10,23 @@ import ( "testing" ) +var backendName string + +func init() { + flag.StringVar(&backendName, "backend", "auto", "Fakemachine backend to use") +} + +func CreateMachine(t *testing.T) *Machine { + machine, err := NewMachineWithBackend(backendName) + assert.Nil(t, err) + machine.SetNumCPUs(2) + + return machine +} + func TestSuccessfullCommand(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) exitcode, _ := m.Run("ls /") @@ -21,7 +36,8 @@ func TestSuccessfullCommand(t *testing.T) { } func TestCommandNotFound(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) exitcode, _ := m.Run("/a/b/c /") if exitcode != 127 { @@ -30,10 +46,12 @@ func TestCommandNotFound(t *testing.T) { } func TestImage(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) - m.CreateImage("test.img", 1024*1024) - exitcode, _ := m.Run("test -b /dev/vda") + _, err := m.CreateImage("test.img", 1024*1024) + assert.Nil(t, err) + exitcode, _ := m.Run("test -b /dev/disk/by-fakemachine-label/fakedisk-0") if exitcode != 0 { t.Fatalf("Test for the virtual image device failed with %d", exitcode) @@ -63,12 +81,13 @@ func AssertMount(t *testing.T, mountpoint, fstype string) { } func TestScratchTmp(t *testing.T) { + t.Parallel() if InMachine() { AssertMount(t, "/scratch", "tmpfs") return } - m := NewMachine() + m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchTmp"}) @@ -78,12 +97,13 @@ func TestScratchTmp(t *testing.T) { } func TestScratchDisk(t *testing.T) { + t.Parallel() if InMachine() { AssertMount(t, "/scratch", "ext4") return } - m := NewMachine() + m := CreateMachine(t) m.SetScratch(1024*1024*1024, "") exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchDisk"}) @@ -94,7 +114,8 @@ func TestScratchDisk(t *testing.T) { } func TestMemory(t *testing.T) { - m := NewMachine() + t.Parallel() + m := CreateMachine(t) m.SetMemory(1024) // Nasty hack, this gets a chunk of shell script inserted in the wrapper script @@ -115,13 +136,13 @@ fi } func TestSpawnMachine(t *testing.T) { - + t.Parallel() if InMachine() { t.Log("Running in the machine") return } - m := NewMachine() + m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestSpawnMachine"}) @@ -131,6 +152,7 @@ func TestSpawnMachine(t *testing.T) { } func TestImageLabel(t *testing.T) { + t.Parallel() if InMachine() { t.Log("Running in the machine") devices := flag.Args() @@ -150,7 +172,7 @@ func TestImageLabel(t *testing.T) { return } - m := NewMachine() + m := CreateMachine(t) autolabel, err := m.CreateImage("test-autolabel.img", 1024*1024) assert.Nil(t, err) diff --git a/testdata/test b/testdata/test new file mode 100644 index 0000000000000000000000000000000000000000..dd33a6b19721565356053d55f7711e4927422ba8 GIT binary patch literal 16384 zcmV+bK>xoAiRB>LET{Bw*dOEh(y4J|+f-&mYFi0Qjc437v>@EV9MB@5Y*0UksD`Wl znLUaoh?ZJ)LL79ss^@pt8U;IqgX$h0TZ3DjsHAS$fcx` zc3jv*Gf=QOrqzNl3-N~qAW3am7l)IF5`fTu(jc(IP4o2wgvY8(wW_&5bOQ;{c618J ze_Hi*SVmf{#_C!#EIe@p>oV$}|4Una=ujwlN@ymr2BM9ixod{}5EGw_GabZ_!{j`n zVZFLOlJX}-p_FAOsch4Tnud%@I5B@}`IzM~E|n{jd7-3LZixV~Xuh6-M(C%=Femv3 z=R?i$kOr{{AV0vnGhzARb1zs#O~A)1A$U3v(|gL}h#q2na#&UW<;tbQsB9&+d%s9ZMJbEV2`KI*oJTGSFRVTDooGc+lTkn|aE*sDZY4A%y&~`0nj2(L1@2*~!loZn>mEGno}ky}R+eVkJ}+L3SG47txn6 z$-v;HKF%LvDKx%5C1JO1Y7Dcs;W3d3jC=%gp4H;h^PPIEwEo3od0&-(BzwS?0#ri5 z>u-0thz-eJdv8#k@ziP^!ovnH^k6I!nLjS6ek7QFV~zxf`mPgAmM=Qe~_7BJqG^LQ;YuU_!cVwG3H10N%}DFzUHI97{V+n-^QB^OtMP zYNmt2rv$%Vgwk>z^L&uU35aT!tFc6?1JbGX&^Ewi;A)>9 zR8OD#U`+4GUbKR_m6S`48#+k*Ok5X#JO#it%bNajhqL|{JJtsRmm^5eKwS0mhneUO zcV>LnSNv_unaiSB8oG5o|6}{Rq589qSAHB;HC-FSc@5@CH|Zo95Xnv>-avnu*@bye z8RNdW0-~B)^OE%GD;5QP_%!5Ub2H;sxI|bIdjmKPo{bu603tl<#~nq#*l#E?1U>^v zBvB6*TIVZ2y~w|%TeLDhq+GYS9@1xR^KZlerq)935~Q(Z1aV=^3gCh1Vr8h>ofdSRw}XAc$nEPZ6RZ;CC=6xG)X&9h~rCM{|y*tBwpJ$@wv~Jf^z|=(& zTRR3(sD??+Q7+`k@=$kD+md&K@qaRIW5rcBO3-(iD=(T;i+tcpRW}%ld2*9Ryo(J; zqdND(#9%p6jT2`3*!xTX9Xa%!QlK$4mwB2NKgBU4FOvtB9y6w@E7}x$YV}=B9 zGn9rHd2vxzCz}>LTMWYAbH5dd1#H=17Q-Gbx*Q_Rr=g2}w)SUR^}JB(YDON6bB@pL z+F-3L!-lJQnFzVWR^ZeV1~j;g+vlZ;2C3bxN65r~PCF#SmJ znFcU39E{yEV)_TUr|7SIciWHk4ni!>17s#lAgmR1Vg243FMG{r)>_}DND1Cfbr=@P z;xF)>Uxl+(Hhr(q=G$670xC|ovQ244wbOpH!W(aIrya7cIVBcJBv+tc>R-H%i`I2i zqXLjkcjs?>9dr!@?yssWzDT^fK$2Y3f|)92y-a{5v*y_03!{fwq9z#Bf|Wz%V8vf1 zTNH0=c(@&$L{k;3mK@K2$)`fV_VM@-c3z#Tlx-1&ZYul1xVNxkwG9p+ zNT0yUGhV()y`!FCAG888-Ws?bO|F_bQEvC3sI|Ce{`{;KIN?hkrdU!e=k#&v;5r4A zwRKMpK>+(+4MiCeT*qqSoP-wOxi7p7?C3C)e(g;rUzr(WS_#D37NjfE4@=~q^Bt-& zerntPbEEPRr`|-W=V|qGzCbj`QLPlqS1u@*+4_t4ixs+BcHMyB0s;WcfTwG-U4+Hn zD(tNw^Bww3@jAsR?g@Z($xj{9E@ifX#Ye?EfU?roQk{W2sNRp?wRxVwP88lh-kHDe z@}?wHreIQu&Ng@C`5V%g3 z(^_ZpK=>vjS!NAJk=R92#+#7xw>TT3tTAFAo`G|7qYzu`F2ZDS9ds`Kr4Vm7Ub{qTxbd zhvaR8(Aj1zYo@rI{TuM zk983T{&0#NofZdJY&WMdAIQ{vfaHx{+_61vk6Xt*{yq z&Y~P+Y;>|v^iHoMBddPcKCu9k@kO)IC&in-GB|;Te@0SpEj_V|Bq))v?0Vy@u!XS& zXN#0ixdLZlI~@GZ*zsLU1aY~9%%l&{&qMzfcrtMNpW9q?;=>R2b>N3h{J|frW-GV(I=<3V!VDtx|PoL=8IoUI8q% z{6>`F&wx$MXu@IKK!0^_=UV2dx|!}{`nslMGjBoCn;VvYv#rXflHXw^uVV)F!6C7w zTui9Y232oeZ5kAXCxmAa;jLNc-ePIJexx^%P9(C&*134A! zI7B&gDurjP`DpNGiLObA-v&dUHiyu-dcD|-%Wk}J+McBMKA{S`^X*@TvNMSbF3#V9 ziT!kcwI^bg~%iGsYgYhCxVBHM78+~F7 z`CW`9@Xfq3iZ()EY@y=w5qGd7;L&@3w^r33VBXIol9B5OrKdlsr}(R*bdV!MCAdF2 zy+YyN;2-v8$4=#xmWU^LZ4G#%aaXe=iQzsR`4Um`vR!8yTnjw*t?YwPcI#=|5G`*M zn6z>K!j+I1vOZbnItoJQ^({p9hITkUEJTksYgNfaq8+eUzYdJ0Wm>& zAa$Ab>e#`+Ejsam2`zj1yHI2tNrRUe$PgXyuHlx~vE$IM3cNQnW7M*f;^j(sLPwE3 z^G>GH5F-b5G0nh<(ZUYZ@asKzAcir-ZOC*t-!vMoLF0Euu+ukgXLKwzIScTKs(@F6 zZK6!(yg=Yo2i(Y#+$${9$t?5`Hx4;^tDEqLkLo#XTpNq_|z0tXpFzwv2n_FAl*6MC>z)zV1u4^sg;V;Rc38TV1takd_i_25;lBd}y- zn=sMN;#@OQWHFuiPco3xgxp6>nWj_vfSXkM*zv!#L=ncPnW{C9OMX?2yizDG(6UH= zM%~k~lh%E_8f2^_5?eD`MFi|5Av>anKkOZC0-)?`1@D=b`Wlu9J(-!+19y*m-o1&r zy^gUy;(VPHJWtY1V;RD4QBup5+WHc!;Y!ctnu-pRCNHf zes#pkpUQx@h6VUTaJ;>_5jcyJm&y1<-|Lt~d}pJ!@qrGy(R_5JXWJsZN)y0UtBU*K z*or}o$?`xHOlscFPx5(&eI?Ym*YqaR(nw@@0|bT;;(j4utOvn~>Trtfx;O<0eO51d zvz;w2;6Ym87qvErwYHwTtT$(vixN&k=EC!kAvlHhl=PN8s>}g-bL&+Skae6!Idm#j zi;Ylb7iAd0g-2 zT>ZO#uFrX;v|rv*5{o2nf=ZUYn*RLv&qI2i4x-B{EnAQg!4sxBR;x{teN$|vy$3QN zx9M~z4Ab%L=hnZ%+C^8R>?7OsJ@Zk-2GEWHkn) zH^@G0&Bg(u5Cne(NwRo)fum7k&P1xd$((Tl$>TbQ&D7Y$NLW=`sn6R(cMb4WRJPq! zC>zmMfrUliyKZycFgX6L%5Jr@Lw9zZZXXDB-tWv7U}33Mh7STGk>9)C6j`-6Q6`Yn zs)L1r`-2~=T_7F%v_9`v;?vRL!S>eVI-OUTr(~Bbb(!NG9zb1;K~L1kYO9ezDMktV z(tu!DYPw5d-sr35KB2RZt%PlmX%tXe$8V=s8BX=6iusn}x&#wVNcEFOaF?r%()!3i(b zF`_-4Cq-{1uZnKl>2#i#*`3B3H}Dv!Q8oZ|4gw+X} ztuLY}Nmg!S%=jrC(r)~xHWtm}9zgl@-m!u4Tr)Fy{wk@nPnOb@ak{xzkb9jsp zt5t7br|i>$^Vp!cajSs*M7wBLy9xld9S~f+ER!utE3*UvH_5A1LbByn@|YpfBPXOy zJ1Z(suEJb~rBjnm!)~#0z-$$4eDQ1Ltq-r?(@&z}2nA>w(H7r7u}YU-)mJ5xlR0JO z&Q`&sisoEVJ3_e6XfL$!;!;dYz_F*+(k|kL-UYW!Ic);k=LIoSLzY$7R?9Ph6N^|i zAzX?nyZw%`xJugI*2(OHjrF7K#Dgecw^0HJOtq)pF`!1GvAxoUIILT?(@!7eXihBY zw`r5-9lm)y2?`g?LiX|YL|uL{(Ha^0{~);k_9;jdxv~q9KGT5xY$rEV0U0)YDUo@AQ81#%L;(45>H)-v{%PMyW z_80$Io6v*pwZEHwNybk`8II!ZI(Zix^gr66TdjH)RXw^BBO0;_Q$1iMmww>iMe zaS|(W#Vvo{liP@QN+6)XCCN@a_Jr*b%9oh2WUuHp2spGb3!Rjxt8_Z%MVFxCP&`5* zMUHpIj*Ppou<3sAHCd&w_5{ByG~zgfBM&Zxg}`ti*t3HiPK-^iT<`T>RHow#NMc-f zZf%oixkc8M4Y$fLVG9GPA%C$?NEJA^5HSn1Vizx@O?@z(=p5;^pDsZEH;cmuV7K~F z5jdewblXjE!v(b`zQFLb_Ed;-JB4@rj;82fRQJx^U&bE9xLV9FkH)t{v&bXRZ=Joy zXi_6uh1gA(9(YY@uLF`vrb;RmdYw<$UQq7(2hWEHteM#ch>zZ?NXWN{fO}u?ySspby@>Yuqw$7p!U>Gqr!Gk>Z6PLznGu z39`nVtK__FenfDkXcz1HA`CP9_} zXW$PKX{NFN^dcjTex%#2kuat%+AJr8&KpVClqtU<`2-Ypkqm9+)g}rB9RwoR^~xNi zyO2Q#lfZ-nl3Jl9dba3K0C&hu@6Of`|v#K|1kt zIH@s5{x~Foy!_#ty(-i~%7UWqQiH@V(@YC)sN0Vri0CIn z!8pWeGlD~A!I~IfKjYL!;YHJ)8MaT2#N`m*7tuZOD1>R!i38V>x_+9!?`fBT@44!+ zElKu0e=z>uC4aykToriL!(nS#?;{VF)mza^{8}v}at7&NXBF_&E7F|-!zLh)yW-j~38+({j-AP51eCfHv`@Xj}n#}9&q z8tuEQ_}sjRX1%$zcSEo!zZKw2k?chc{?=i$Qn*TAiMBWnq-h?n<$6+7IysO|$#C6I z5-4Lxg=~N1r2|eP<%Ydgxbs51S9*U;1r27#<{}4)_DF>^;?}nfQMW>0wrS1 zFc)s|i@(-2-r@i55aF+4zFDN^nlm7+WUH{{dCykHm|@N*7RnP}9xIl(^dIrsv?7b2 zC~@Z}0|4yX6#&vl(ds$erF*M(vGEvvz76&H>&Qnt@15o&y3sQ1ShWFhWzdMRh7G0h z(fbcJv=?wHfD}25apLY`FJlh<7%*|fiq^a^yajK57RwfvAsw^bf-D)FURDWKGp{c- zyL?8`%5RghB@7pm`1OlpEA&Tz>clo=tDVn*d{50DHfF%+6H}EX7Ai(N@Ud)e`GwpG z6_N$R*-jnlte*06$XuW8zEIR@9kd`jD*~&>%6?iF$W5?$<2ReTvLAj8;?@?ipmt5# zst(@csRFC?pzcRm90CucSmLoYvtfQ@1x7!XYur8j2{zFppv8bXp+u&U_%#Pd3y?{R z<<%@L@ntfC1EuRi&&+t2X*@9BED-5t(e(UlO{GuuP_jpW;DBRN*^<&h%rrNB05xh? zKkSH2szum-=L9XEmvA=HlT|4(WXZ`Wfb51B4mT9j-+1h4bPHhkwGUDBSHrIIqU*)k}M3CJu?pY4Wzh{PB6j| z;=)gWajsEpTWJVbmZERiq0`F7rIJ|>dB8w7h6d-9T6zy*PP@EQJ%A!@oH2s8C9Tnf zr&%OF-m|e2SP@m;r3mJ}F%@E%^fKw|_JTcx<-I|7Z=UywNgE#t&O2fC|v~)lMiUME#zlBcS zzUq;8xwS!{YoLJ2r8TfmSKU$MU1aWb_-=OE_?A#ikq1t__oVsD%cThtbi`yINYMc3 z9g+hUrGUYS`CCIFHWvd1)_pC{T96yEha$X}J`BnDaI!k|?aa8i`)+(I)3A2`J8=h` z6!>=#*8a~6>db0jh?sEzzjDnO7e-QUDAz*6PkwF+8-kq#ACa(P@^a+RW_J`e_1hQ| z0Dxa4*#P6qE{U6XuSXbH{KyU;H`}Cf7sNm(`M_#HYoINR2H}zOX=em3?GJUXRe?;l z+$EEMC@!qz@-3?STo!J&?BhWA zUuM~qOD5Tm6+yY@^+m1`oLZJ?|DhNXT-jZYFlKbSMn0lf_FIxa%;@eieB3QILuOZ2URN?kUAK`F)e)t7Wec4(=mku~_@snt z4JP1tOOJ&F8%*B?MRV+_s*W*X#x~VJ=Lf-J-uLd{?ZWCJZHeKmSTe95iL?jgpe`h3 zN(SuG2ZKgKY-{S#UFL*)iL0B2$C!uCwm_Mf@_(wCE7`~1M4-QEzZz`jA+tJH(RqR7 z*nD6rKdf^jGd4GEw#1Y$Pmiy>gp3vch96S{Ob+6&o}W`JN|gi9jAh>Uo;ZjYvVUn! zv%5hqrFnU^LYK#9IwVDLyxhla()_@Uhp1wqQP0j$Aqm9R6e`^6^%YAZ!C&HxYCFCP z^wH9Sq=UOS?J*cHD(@_Y^%&bDVn8E1`?k`cf;#`0vV1a+yiLzg>GJFXU2^0NH;N&?aitVqG8>;Mdm0n~tV0=we`uka- z+XZ%-Ehn+fof#R~mX@|TZo`{g)ZK5TBg>$d#LFNNfjLNn4I+pm{0pbHE{jeYPCI<- zwe948h(1}^yTJfW|I%><)j|Na}GNHR4oI5B^AwMp+`WKdl<(fNOH6pTVIcEQ5;Jbyl!u{d5D1Rv)?1a8D8V zX^Jm$$25k4c}$47Oc^{`9O>MkzCtI+!F>0QveH0=_3n2>`*CO_S*kBEqxhC-tv z#KMe))9y`TZeNMpa$eqObUmo|lyYO(g?xOwQPr5j6?iX%9W)ne#_j`8H_fdd8_MYa znl6e_SY-U7AZ`$DjlZ}Hweq0X@3b+CH)xiLb1+Pex7Y~r^?>knqqOr5#_yZHZZ-mm zb9o>{r(2q0?Zi7NAzR}At}!tE=ZVRoYa03UJnnUl$O>YjdSm3Q5CaH2v1vF%&Ok_u zhTY8GKuryqOlDjIu4Gn24^1>!Z3BYI%^cm#Rg=zGDisB5zTF|lIn6ng&M-3+C#|Jr zvS-6tTd|w>1yXu0Vt4S1Lx&%t5b2_K>i|2YZvWWr?6Ib1`NKl7rge~>Shh7KOj;h8 zu&J>b4h6E9-Gy#houMr2;Hs=xP4cWdLm>*X)*qj(yI|ApA`CF4L>ToHU7C(fwIj)K zr>UtlhdLNG)owjwe>0NLxX$gBeuD|O!y3sH7L2~@UXznX9=?ei#(crpVm3D(`) zYYNkWVQitJs+(@N*Hxi+Edn8sNf8Hf5b?T}Du5$@6L(CRrX2oa_D5Wyyx%Pf_2aQ; zcKiY}GcfocYz;tGan4uXa72jcZyp>p#bp9X*Uw$ctbYs;&O_V!P08!>ZKERahcmV%M(|gEa2C6rZeJFyXsUZ!Yuwhg&Pkgvbm*pdu}?X!I@tYMfVMe-VFWTJZP(6Af_(VJsWW5skq0M&eoG&oOMDchAz0tUeN( zcym%+He6U}=vAqvKeCPRL2m0?_b+te@uL&vNq#Q80_3dIt|uFOS?Huz!BK* zm;+=|Ot}xl-ZK#i(|hDG2CwySm;rKkbi#lL5WLY-&540tM=;2KRS$njJ0JdGpaQ#tK?C=*B6Hpe$X;->_04$des7VdT4QrtTZs0)AFPKt>8G;#ld7f z=$Bxh$Sl%yV1liuHc0zD@i!zHD@{{uOH;low>zse>O#XcCBZFeDgnU zHSSTSR{ap9LKdJuqP_vgEZx?|rR~G{Y#(?%pTz^+f6B!D(o<$}iydgKQ#)sceefz) z)`Rb7Kp>6%(@L|TV}w)pHW>r$Z;%WH$7hX2*~iCjYKf#E!j$FADP5)rw`TN7QSqpV z1pH(LEk$4BS)`> z#QRJ;Bt*!&5A8e3e{-B|ifki3C#9kq`YQ=NTNhJy%~oX)4x?__pmR-npG|T!-6D)1 zj!#juY_gqh)%PQJ;cNLNG&gK&C)QUJ-nUlh_G@rFnYgVzS6Reee)Axa zpTXj27crX3q~w|(;ud2(PjzGxbRx$zrsn=Z)YFPL3B8}j=>Y8ZtbdrFx-xewwj&P(2R7vfBnQGS z)*D~{l*7_{X9$3avA)HDNEN7GMjGBpd>HybnNQseN|$8=(wW4Qv(V^3RK9WyZmmZE zeig3z(OW=8q@X|tYe{_3sXJrsTJ=)mPB$-**6l*GB@cd1Vuu5P ztXTW}L~FwMHq$NtqFj->QkTE8m;RF_4LZ8jBJ`FS?CpyMjrJPBq;EnY8U+r<4p48< zy2PMAmL_`#ThPz*(Ax+;+zjL?B2<$RoFh}}e`j@YE(_rN4JYa<^IJBXR^4e{O< z4v-@4w%zmi-j)KaZVJFk{d3a?}H%>X5SXOGA1Wwp}>~HRNEh%qU-#Z zoFKOsb@`RnS=I}TJ-O3?A?4N#4M>#lSCJqlPbNL|ECsVF?tIf+7%ZR!_+<;u7eOg4 zv!!nsMWr0R8y4}NOG71Raui1>#ANOg$kK4-&Uv@{3#PzQ`E?9&Rf6ZizFxT=0Svjy z?FHpv0nL~PecqI0z};3<)%ec!V7z)4pwa`6Dqm)}sZ@)T# zwU~RDaUVgh0TgJix8%~6(zH2f;jAP_9PjvD0=j`}M$W~SKAsGl8~VVU*(Ly_0}gLT zVy+pBH1w2vsd>}989vkWg_)@y)aK>C(W$F_Hv4;8>%VWRn4ZK?Puu6Wup zP34^DGpdlZo|}dJO#$hE*C-x@0k8cpLhBEkz$+dqJqT0LDR1~YYDkiiw)a~`8Z{8K zoQh#{zJ008(SKc+8OU|)0pm2)rRXfstzC?s=G1g!mKa_k?m`PAH|$so5>dVs+RCq; z&1q4Iq5enO#23AtIe^SyP-ZFP`5h#H0hrZagf`4?uuPpLOoNt!hu?e|7J^7fXOLzeCHZWAh$}a=&yE;J%~9XjfYCS)na*+rRmY^km;Ky%xot{Th( zX69ybEKbu1UX1>eJ9e+aCxQG7ElL5R3T70g!(@|&+OS`>$9+((yocLSAsP~}>wE*^ zS)iOZ<~wd8YJaJiq;HVw1$psLE{9&0lVWo~@v7Q*_3wMXmDIWV{;S6>W@k>Q7Axo>Wjh zJMG2EjnNg~S4t7KT4|1clPkQ0=W~GuodkyiNs+7|AjX`m137+Oj>t{-z-aHy|7JH? zs$~W+b3~Y{+dnTX*|xteuAieg1x^6s|GWf&jKxu<#5v?JQ0#vBV`?-vg_fl=E;89&KeK^5S3$G7t-TVSqHlk(ONe8u! z+2e@rF`>&@cUp#ID$x5xzKI5Y8W~QN1-lOhDqKj7 zfIFc08*|SUMenecZZgHd;3~awHbjzu133VdD7UK`Tk6#aRKJY*x(wr*$$i+LnvYIT z$HB}G!`B%82a|DDxrEA0{d1ihlN^lM9Epbf82w>m=GZPZgBO$0AX)?`x$f$$^p%W;M}?I z*Cue~LoihMTYql7J4Pf(#z>bvDhjtCwY(s_&*-?ufm*{WhUOcVUIO8RG~jNqF(_#= z$%53wHAHzFKHv%uaM3aOY|KxbQYoycex2l%S`aZwkNO$P&v4yBN`!DXH~!hO{GpE8 z^ro@ygi*#**Hf71I%fD^3_59A8{AerSJAtmj}(L&T^3$Qd5V~u1S59!hO5neFTOZd z3fDv)^>9{JNR_g`=_*xT+vIhxcl$F^kg<`sDaQ(;PC2JFzt6IZ#mc1AJ8=L4JUj%F zUE(7Z1OE6Q681n(e*dEPBEGbBp81Q@52*&xz}E5=I>&f@^!o=$??Z7eP9u8Jvg|M! zU#x#(c56tCu@`zLwu}avPpc|6_|DqufDKgg)!?F+>hL<34a!HwT8u zC!}J^(*O34JW2-OF@wsuM3U5o%?1??9-tby<-Ard27vyrch0c$lBzj{dT7v0?}oRz zKipbTA$51>)GX#Po%gU1ij8!II9HgS^udX@&9)+E!jxLm%b3v!6XD`TFxz38|KVv> zF6m)Xyt$2v;aiJWvcp1^dORI+KMByQN+O8@9@w<09?Z^ZR5I%AATU(+_N$<2v>|=odHq%wT(!uoQnP=1B#wjfUr^Dv< zAWTYW~0T?HY1pJe6WodHh3y5^hKN` z!#d*BuMM*aDfIt@h}ruwWXZD`xn+D4TJ6yik*UD|i6mZm!!rMTHe-`0NoIva1x!dAnzt4E$=R56G7^jz>PI}F2?jJzPODZQ2PgFgCuNJH>2L5dl(s%CgN2<3G^3|G>$JQd*M>D zd~mvoW3KHE`-;t@?1JNA7#h?h4Fae)Q$60jmPMzqToD|QePfg+-Y(;nX%8tO>4L{b zXCP9-9)a2d zrAW!Bcbd-wI=}3z(hxQ8udCZZS;8h+8@TLLwoy`94^NcOB%h*jVs+RU6LDDTts*KD z`I97-{xg3U9w#lU?pfOqxEqds$sbPAJaP7JU0~|A+pl7u=eC9! z@jrrGh32`jTyQ8RRw=5m8Hq_Aw;Qb|Vx9Z@p7ATE0uQk8kuA z1J2mt;SXBE79)AJhREYO6&tQMUSz>-yt9CzZQO3!lEfVpUfoJ;FJ>BtCWx)ot~-Zr22 zGNrc1++30B-e|WVpGwa}7d^vz7Z*M*z-A3we~$s8;eUc+tK*lj^A=~pB>pGUf9QGGbN}DGI4&6ky?!SXt(9%r*IIF<{#La`_O8(w zqAZ`Yx~+dfum~n90&38XOLTyY+s6X`Hb{D>D0im}a|g2cr#6;^2M<(3<-mMG$-11k zbEeqhv^Sd>=A>9@)kA8*!0yvkh8Y6ik1GfSRfNl&!{vPIoxOMDz~|#=RR7aR@0K$5 zjW$3b%i28O5C&{~i zuNuoxF2k?W7R9YY%kk634?Ti5+-l82D{g4+e`a0(Pr7dcRfxE|mE`dtZ|J4S=AOW8 zLtzv$q1*pIxPoPvJn6y-Yo}8ynND74LyGO>52RLoB93}QK#UMUMu#L>ZEfW4D0eQn z)0orB_OKFhp50!QyM6|2u+fC6U;BOJ1iw5{6TtIjXLnYVunKz~AF+?avKqZ+%clR~ z`a*{g^Xg`BvD79xgL;FHrkg|jzTD>0&DnDt(NDo5hq#1hF1%dU%z%A7!e3xtg6OVD z8em6H=_gFs=}>ucQi+u6^@8zHaks30kgg4br?v#DMRXx@5D=j)X=)`-1S_*Fn7(WP_U(T! z$PS%pwvA7|$h6pGag75dYanOX)_`zMumdk@T3iNHNJbEKaWhl*4SxRG)B&$`x>u&l zEMoL8fsOI1Bjq8_H6N3m-8DoMlI98PH(uQCqXiy5Px`^+MpxkIL7tim79{fL;Be%L zH%%NFV^DS3_?N@aNcaJg(HY(3mD!5d;A(-emVuqY0~=kWpF~KAK`h;9W_f1WXA-+R z*`B(6;m(!chL|r|jlZx?lMHycPys6~N#2+36cP)vDz^{L2=F!jx5lRw#`LE4l|zj- zGt{pfnRp{`MKdlQcXm1@{gZW&r7GJIuE~({WZKAYt}6J=-Nkf-(NEVp5Ww_5e(FtR zNHh-Q&n;GB^y&Rlx(&JNp-YtyhrMnJ49?HTE1vuXZKD!jZ5o4gYJL-27SJYefEKYU zrQIr8)M>8N!pq9+Ooi5%B za??yT)S{@n-XZ4^vUPDLXe?s<2=S-8*||D^AfzzV#=n-l3d(siYYOCNPi=O}%wKe8 zriha$yB+6w_u}yjGFpidKNy}3M(W2HUGjH}_p_J0B^mBqCE|qDn2~KO!-GEp7O?6V zeq{dEX$!8>-5)SP@4AaPus!*6o?}{X&Fg8+w$j}YtUTAJ`m{0DOgwQl-i-Zn61VvQ zcX#nPMX8c1Pl7PgT&-Dc$ZS(!OMNhcGp!<>N_=^+% zME%7u7EXc(A9lcXN*Pg)zT`CV;Yw^{+nZE0&F(PsO}2Ey9=<8FFTi|EYhWtkJ?GZfK5`O(yP%oE7=k`hNb_&Y? zYJ?&*7cD|IoN1}z@H(D7m*);p=<_h zmqRWOP|B}5Y8PDHO0a_B=HcMl?4vTgUK4ooOmF|^0>Gzdp3WfFX9E!5molwy3$4cW z1BV&_(y+nZj;DE{ABfB9_>Q1ZQvcZXunzrBl?e zJW9JKg!HX*AD3pu@sehLJkT!*7`tEN`<;ZOIZABVm>GQY!&qJ(SW3q{9;#8QMlhpgr*eF;++sv6}6{Y~dPzz#o_)jahlUjl&= zl|#lSbdFR-Io+jWAM7$2452L4ZDHqZ(%7Suj?J$H_mN&%bbbWW_5Y?FCp;Xp2FDR zgON#+_Ejt)%|BruU8Lu~_}{exX-@lI$WU?Xi309Wi`9F68+I}+-NV1{{K*Hgrq6Fb5MZlO4WR^e)Olss(7ITAEKjwV{ z)$#8mzu5#XgI2J1SkDgS#e>s9+3f>;(?cR1`qBkM3YAM}_vhycupbyh4~7HDbj*u* zvw$}Fl0h*U%*ej}bJh;!sAXr8%RLq|Xuh4`9%I==3U?#6dsZ9KLW?@E+o-3z3Noex zV+`rIvaA}gF9N1%3=oq~v*Bcd5N&*&!o{4>?r&@d4tyzcpM{H9Zz@g9dQ`}#mX{Jk zIoW$JF^ObZXkS#Q;6dB&ZX|ssKJo8OXUGE)N!62OvvfHW4e0bS2YVx4Y4-Yv1Q2^h zz4AS|+2Vp=L-j^J`6IMGTErC;m)FayhR@@cEd`z3Y zn1sq+^r|y1cHU(2pNlL^Q`iGxSd}cxM3X!MbH1oNH^k)`d$tUJ&yskPOVacHpMIb_$*Y#0B3btljZ}Fqss*UkSPeypyM|39 z`uDYKq5LSrPAF7i0vO_h<~hDBzs)*m=Eq|5!CvM0P^qK*&E@?$L#u#3R#jY~p7IDQ OS^>N%WT!C(j-!9Eu*}8) literal 0 HcmV?d00001 diff --git a/testdata/test.gz b/testdata/test.gz new file mode 100644 index 0000000000000000000000000000000000000000..91d59b52487283553deeac8c531935d5ad66033e GIT binary patch literal 16412 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 literal 0 HcmV?d00001 diff --git a/testdata/test.xz b/testdata/test.xz new file mode 100644 index 0000000000000000000000000000000000000000..6a17b3a5c3fa735b41660993d14f529a890e754e GIT binary patch literal 16444 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 Z7#kWGBUQSmZ9J0xn+bSE00T>86abq-;6VTY literal 0 HcmV?d00001 diff --git a/testdata/test.zst b/testdata/test.zst new file mode 100644 index 0000000000000000000000000000000000000000..5641b91682af1dabcb8dd7ac3721d67143c366ee GIT binary patch literal 16398 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

>C@vkYMDT_4;tJ#+(pX}5(szwLZ9Lo=U=>bd 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 literal 0 HcmV?d00001 From 5c1fd97ff4f7b5be3d71c2ad4c7b5b1164c806d2 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:19:01 +0100 Subject: [PATCH 12/52] Point d/watch at tagged releases In debos/fakemachine we are now tagging releases often rather than expecting users to use a git snapshot. Remove the git snapshot stanza from the watch file and instead watch for tagged releases. Signed-off-by: Christopher Obbard --- debian/watch | 4 ---- 1 file changed, 4 deletions(-) diff --git a/debian/watch b/debian/watch index 73d7328..afc8218 100644 --- a/debian/watch +++ b/debian/watch @@ -3,7 +3,3 @@ version=4 opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/fakemachine-\$1\.tar\.gz/,\ uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/\$1~\$2\$3/ \ https://github.com/go-debos/fakemachine/tags .*/v?(\d\S*)\.tar\.gz - -opts="mode=git, pgpmode=none, pretty=0.0~git%cd.%h, repack, compression=xz" \ - https://github.com/go-debos/fakemachine.git \ - HEAD From 0afe01f261a06ff79258f2c0440f38ea80c949eb Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:32:48 +0100 Subject: [PATCH 13/52] Add missing Build-Depends Signed-off-by: Christopher Obbard --- debian/control | 2 ++ 1 file changed, 2 insertions(+) diff --git a/debian/control b/debian/control index 040b411..a130942 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,8 @@ Build-Depends: debhelper-compat (= 12), golang-golang-x-sys-dev, golang-go-flags-dev, golang-github-stretchr-testify-dev, + golang-github-klauspost-compress-dev, + golang-github-ulikunitz-xz-dev, # need for tests # busybox, # qemu-system From 08a2b2e8f72ad3804f86fb7e6f7cb0bc2d07539d Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:58:38 +0100 Subject: [PATCH 14/52] Bump d/changelog Signed-off-by: Christopher Obbard --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2759693..332e5ad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +golang-github-go-debos-fakemachine (0.0.3-1) UNRELEASED; urgency=medium + + * Team upload. + * New upstream version 0.0.3 + * Point d/watch at tagged releases + * Add missing Build-Depends + + -- Christopher Obbard Fri, 21 Oct 2022 12:58:13 +0100 + golang-github-go-debos-fakemachine (0.0~git20210901.fc48786-1) unstable; urgency=medium * Team upload. From c19213fa716441f339e40fb0a4502c0a19e37664 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 13:00:18 +0100 Subject: [PATCH 15/52] Upload to unstable Signed-off-by: Christopher Obbard --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 332e5ad..d4281e6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,11 +1,11 @@ -golang-github-go-debos-fakemachine (0.0.3-1) UNRELEASED; urgency=medium +golang-github-go-debos-fakemachine (0.0.3-1) unstable; urgency=medium * Team upload. * New upstream version 0.0.3 * Point d/watch at tagged releases * Add missing Build-Depends - -- Christopher Obbard Fri, 21 Oct 2022 12:58:13 +0100 + -- Christopher Obbard Fri, 21 Oct 2022 12:58:57 +0100 golang-github-go-debos-fakemachine (0.0~git20210901.fc48786-1) unstable; urgency=medium From 2289a4d6b8df49aa9e310f0aea6b92c6849fe3a2 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 14:23:49 +0100 Subject: [PATCH 16/52] Add myself to list of uploaders Signed-off-by: Christopher Obbard --- debian/changelog | 6 ++++++ debian/control | 1 + 2 files changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index d4281e6..57b203e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium + + * Add myself to list of uploaders + + -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 + golang-github-go-debos-fakemachine (0.0.3-1) unstable; urgency=medium * Team upload. diff --git a/debian/control b/debian/control index a130942..fae38d7 100644 --- a/debian/control +++ b/debian/control @@ -4,6 +4,7 @@ Priority: optional Maintainer: Debian Go Packaging Team Uploaders: Andrej Shadura , + Christopher Obbard , Héctor Orón Martínez Build-Depends: debhelper-compat (= 12), dh-golang, From 9b7cb4bf8b87f5ad78f869504c04caa1b398192c Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 14:25:31 +0100 Subject: [PATCH 17/52] Remove comment from Build-Dep Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 57b203e..c573b1d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Add myself to list of uploaders + * Remove comment from Build-Dep -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index fae38d7..23a4f00 100644 --- a/debian/control +++ b/debian/control @@ -16,9 +16,6 @@ Build-Depends: debhelper-compat (= 12), golang-github-stretchr-testify-dev, golang-github-klauspost-compress-dev, golang-github-ulikunitz-xz-dev, -# need for tests -# busybox, -# qemu-system Standards-Version: 4.5.1 Rules-Requires-Root: no Homepage: https://github.com/go-debos/fakemachine From 8e277ab7b3dbb5399f5877c489a50d4162908c5c Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 14:33:22 +0100 Subject: [PATCH 18/52] Depend on go libraries For any projects (e.g. debos) which depends on the golang-github-go-debos-fakemachine-dev package, it should bring in the dependencies which fakemachine library uses by default. Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index c573b1d..58e4181 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Add myself to list of uploaders * Remove comment from Build-Dep + * Depend on go libraries -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index 23a4f00..c6d08fc 100644 --- a/debian/control +++ b/debian/control @@ -39,7 +39,14 @@ Description: create and spawn virtual machines for building images with debos Package: golang-github-go-debos-fakemachine-dev Architecture: amd64 Built-Using: ${misc:Built-Using} -Depends: ${shlibs:Depends}, +Depends: golang-github-docker-go-units-dev, + golang-github-surma-gocpio-dev, + golang-golang-x-sys-dev, + golang-go-flags-dev, + golang-github-stretchr-testify-dev, + golang-github-klauspost-compress-dev, + golang-github-ulikunitz-xz-dev, + ${shlibs:Depends}, ${misc:Depends} Description: create and spawn virtual machines for building images with debos Create and spawn virtual machines for building images with debos tool. From b7531bd64249d6c5a1bab9772ec136caa1b693ef Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:23:05 +0100 Subject: [PATCH 19/52] Depend on systemd-resolved (Closes: #1020690) Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 1 + 2 files changed, 2 insertions(+) diff --git a/debian/changelog b/debian/changelog index 58e4181..8565abb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,6 +3,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Add myself to list of uploaders * Remove comment from Build-Dep * Depend on go libraries + * Depend on systemd-resolved (Closes: #1020690) -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index c6d08fc..024e270 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,7 @@ Built-Using: ${misc:Built-Using} Depends: busybox | busybox-static, qemu-system-x86, systemd, + systemd-resolved, ${shlibs:Depends}, ${misc:Depends} Recommends: e2fsprogs, From dec7bf90d4a5000524137ddb5e43966f18ae63de Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:23:15 +0100 Subject: [PATCH 20/52] Add user-mode-linux to Suggests Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 2 ++ 2 files changed, 3 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8565abb..d2c0bbc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Remove comment from Build-Dep * Depend on go libraries * Depend on systemd-resolved (Closes: #1020690) + * Add user-mode-linux to Suggests -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index 024e270..8ecfabc 100644 --- a/debian/control +++ b/debian/control @@ -34,6 +34,8 @@ Depends: busybox | busybox-static, ${misc:Depends} Recommends: e2fsprogs, linux-image-amd64 +Suggests: user-mode-linux, + libslirp-helper Description: create and spawn virtual machines for building images with debos Create and spawn virtual machines for building images with debos tool. From 8ae5fdb386beb51f1ec01114e64278a9f49614a7 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:28:56 +0100 Subject: [PATCH 21/52] Switch to DEP-14 branch names Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/gbp.conf | 2 ++ 2 files changed, 3 insertions(+) diff --git a/debian/changelog b/debian/changelog index d2c0bbc..36bd73d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -5,6 +5,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Depend on go libraries * Depend on systemd-resolved (Closes: #1020690) * Add user-mode-linux to Suggests + * Switch to DEP-14 branch names -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/gbp.conf b/debian/gbp.conf index cec628c..df4a5d8 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,2 +1,4 @@ [DEFAULT] +debian-branch=debian/unstable +upstream-branch=upstream/latest pristine-tar = True From 0f609d9fe8aeab1ae8a1ce71ff8843e9c39abfa7 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:32:25 +0100 Subject: [PATCH 22/52] Update package description The description states that fakemachine is used explicitly with debos; that's untrue as it is a generic library. Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 36bd73d..55937f1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -6,6 +6,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Depend on systemd-resolved (Closes: #1020690) * Add user-mode-linux to Suggests * Switch to DEP-14 branch names + * Update package description -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index 8ecfabc..221cf1b 100644 --- a/debian/control +++ b/debian/control @@ -36,8 +36,8 @@ Recommends: e2fsprogs, linux-image-amd64 Suggests: user-mode-linux, libslirp-helper -Description: create and spawn virtual machines for building images with debos - Create and spawn virtual machines for building images with debos tool. +Description: create and spawn virtual machines + Create and spawn virtual machines based on the currently running system. Package: golang-github-go-debos-fakemachine-dev Architecture: amd64 @@ -51,6 +51,6 @@ Depends: golang-github-docker-go-units-dev, golang-github-ulikunitz-xz-dev, ${shlibs:Depends}, ${misc:Depends} -Description: create and spawn virtual machines for building images with debos - Create and spawn virtual machines for building images with debos tool. +Description: create and spawn virtual machines + Create and spawn virtual machines based on the currently running system. (development libraries) From f277fb9b7cf677d003d2327719db35cbc7e6e1b3 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:43:21 +0100 Subject: [PATCH 23/52] Sort d/control with wrap-and-sort Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 61 ++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/debian/changelog b/debian/changelog index 55937f1..5b5c14d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -7,6 +7,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Add user-mode-linux to Suggests * Switch to DEP-14 branch names * Update package description + * Sort d/control with wrap-and-sort -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 diff --git a/debian/control b/debian/control index 221cf1b..bc3644c 100644 --- a/debian/control +++ b/debian/control @@ -5,17 +5,18 @@ Maintainer: Debian Go Packaging Team , Christopher Obbard , - Héctor Orón Martínez -Build-Depends: debhelper-compat (= 12), - dh-golang, - golang-any, - golang-github-docker-go-units-dev, - golang-github-surma-gocpio-dev, - golang-golang-x-sys-dev, - golang-go-flags-dev, - golang-github-stretchr-testify-dev, - golang-github-klauspost-compress-dev, - golang-github-ulikunitz-xz-dev, + Héctor Orón Martínez , +Build-Depends: + debhelper-compat (= 12), + dh-golang, + golang-any, + golang-github-docker-go-units-dev, + golang-github-klauspost-compress-dev, + golang-github-stretchr-testify-dev, + golang-github-surma-gocpio-dev, + golang-github-ulikunitz-xz-dev, + golang-go-flags-dev, + golang-golang-x-sys-dev, Standards-Version: 4.5.1 Rules-Requires-Root: no Homepage: https://github.com/go-debos/fakemachine @@ -26,31 +27,31 @@ XS-Go-Import-Path: github.com/go-debos/fakemachine Package: fakemachine Architecture: amd64 Built-Using: ${misc:Built-Using} -Depends: busybox | busybox-static, - qemu-system-x86, - systemd, - systemd-resolved, - ${shlibs:Depends}, - ${misc:Depends} -Recommends: e2fsprogs, - linux-image-amd64 -Suggests: user-mode-linux, - libslirp-helper +Depends: + busybox | busybox-static, + qemu-system-x86, + systemd, + systemd-resolved, + ${misc:Depends}, + ${shlibs:Depends}, +Recommends: e2fsprogs, linux-image-amd64 +Suggests: libslirp-helper, user-mode-linux Description: create and spawn virtual machines Create and spawn virtual machines based on the currently running system. Package: golang-github-go-debos-fakemachine-dev Architecture: amd64 Built-Using: ${misc:Built-Using} -Depends: golang-github-docker-go-units-dev, - golang-github-surma-gocpio-dev, - golang-golang-x-sys-dev, - golang-go-flags-dev, - golang-github-stretchr-testify-dev, - golang-github-klauspost-compress-dev, - golang-github-ulikunitz-xz-dev, - ${shlibs:Depends}, - ${misc:Depends} +Depends: + golang-github-docker-go-units-dev, + golang-github-klauspost-compress-dev, + golang-github-stretchr-testify-dev, + golang-github-surma-gocpio-dev, + golang-github-ulikunitz-xz-dev, + golang-go-flags-dev, + golang-golang-x-sys-dev, + ${misc:Depends}, + ${shlibs:Depends}, Description: create and spawn virtual machines Create and spawn virtual machines based on the currently running system. (development libraries) From 51641040c8710451e40ad3a3af8d2498d0c32a44 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 15:48:33 +0100 Subject: [PATCH 24/52] Release 0.0.3-2 to unstable Signed-off-by: Christopher Obbard --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 5b5c14d..29ac65e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium +golang-github-go-debos-fakemachine (0.0.3-2) unstable; urgency=medium * Add myself to list of uploaders * Remove comment from Build-Dep @@ -9,7 +9,7 @@ golang-github-go-debos-fakemachine (0.0.3-2) UNRELEASED; urgency=medium * Update package description * Sort d/control with wrap-and-sort - -- Christopher Obbard Wed, 26 Oct 2022 14:23:49 +0100 + -- Christopher Obbard Wed, 26 Oct 2022 15:48:26 +0100 golang-github-go-debos-fakemachine (0.0.3-1) unstable; urgency=medium From 566b38a8efdbf1453c672fb4b48d3a341738c4f9 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 18:08:35 +0100 Subject: [PATCH 25/52] Bump Standards-Version to 4.6.1 Signed-off-by: Christopher Obbard --- debian/changelog | 6 ++++++ debian/control | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 29ac65e..1187429 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +golang-github-go-debos-fakemachine (0.0.3-3) UNRELEASED; urgency=medium + + * Bump Standards-Version to 4.6.1 + + -- Christopher Obbard Wed, 26 Oct 2022 18:07:53 +0100 + golang-github-go-debos-fakemachine (0.0.3-2) unstable; urgency=medium * Add myself to list of uploaders diff --git a/debian/control b/debian/control index bc3644c..acec37c 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Build-Depends: golang-github-ulikunitz-xz-dev, golang-go-flags-dev, golang-golang-x-sys-dev, -Standards-Version: 4.5.1 +Standards-Version: 4.6.1 Rules-Requires-Root: no Homepage: https://github.com/go-debos/fakemachine Vcs-Browser: https://salsa.debian.org/go-team/packages/golang-github-go-debos-fakemachine From 2216c69785cff5c60ea406ec7569f3e7f82e6913 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 26 Oct 2022 18:09:36 +0100 Subject: [PATCH 26/52] Downgrade systemd-resolved to Recommends Currently installing systemd-resolved as a dependency causes the autopkgtest runner to break. Signed-off-by: Christopher Obbard --- debian/changelog | 1 + debian/control | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 1187429..cb670e2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ golang-github-go-debos-fakemachine (0.0.3-3) UNRELEASED; urgency=medium * Bump Standards-Version to 4.6.1 + * Downgrade systemd-resolved to Recommends -- Christopher Obbard Wed, 26 Oct 2022 18:07:53 +0100 diff --git a/debian/control b/debian/control index acec37c..952a9fe 100644 --- a/debian/control +++ b/debian/control @@ -31,10 +31,12 @@ Depends: busybox | busybox-static, qemu-system-x86, systemd, - systemd-resolved, ${misc:Depends}, ${shlibs:Depends}, -Recommends: e2fsprogs, linux-image-amd64 +Recommends: + e2fsprogs, + linux-image-amd64, + systemd-resolved, Suggests: libslirp-helper, user-mode-linux Description: create and spawn virtual machines Create and spawn virtual machines based on the currently running system. From 3d15aea336b48aa2ec235be4b5c4f3952ceb3acf Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Thu, 27 Oct 2022 08:59:56 +0100 Subject: [PATCH 27/52] Update changelog for 0.0.3-3 release Signed-off-by: Christopher Obbard --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index cb670e2..d60c8f5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,9 @@ -golang-github-go-debos-fakemachine (0.0.3-3) UNRELEASED; urgency=medium +golang-github-go-debos-fakemachine (0.0.3-3) unstable; urgency=medium * Bump Standards-Version to 4.6.1 * Downgrade systemd-resolved to Recommends - -- Christopher Obbard Wed, 26 Oct 2022 18:07:53 +0100 + -- Christopher Obbard Thu, 27 Oct 2022 08:59:53 +0100 golang-github-go-debos-fakemachine (0.0.3-2) unstable; urgency=medium From 127c9f205fb6b17c8beb53576de06dc336f7b14e Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 09:57:19 +0000 Subject: [PATCH 28/52] New upstream version 0.0.4 --- .github/workflows/ci.yml | 22 +++++++++++++ .gitignore | 2 +- README.md | 11 ++++--- backend.go | 6 ++++ backend_qemu.go | 2 +- cmd/fakemachine/main.go | 6 +++- doc/man/create_manpage.sh | 26 ++++++++++++++++ doc/man/fakemachine.1 | 50 ++++++++++++++++++++++++++++++ doc/man/fakemachine.md | 31 +++++++++++++++++++ go.mod | 4 +-- go.sum | 8 +++-- machine.go | 46 ++++++++++++++++++++++++--- machine_test.go | 65 +++++++++++++++++++++++++++++---------- 13 files changed, 246 insertions(+), 33 deletions(-) create mode 100755 doc/man/create_manpage.sh create mode 100644 doc/man/fakemachine.1 create mode 100644 doc/man/fakemachine.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a51287..89af7ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,11 @@ on: push: branches-ignore: - '*.tmp' + # Build at 04:00am every Monday + schedule: + - cron: "0 4 * * 1" pull_request: + workflow_dispatch: jobs: golangci: @@ -16,6 +20,23 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 + man-page: + name: Check if man page has been regenerated + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + # Don't check the diff of the final manpage, instead check the + # intermediate markdownfile instead as it is a lot less likely to + # drastically change with different versions of pandoc etc. + cd doc/man/ && ./create_manpage.sh + git checkout *.1 + git diff --exit-code + test: strategy: fail-fast: false @@ -64,6 +85,7 @@ jobs: if: success() needs: - golangci + - man-page - test runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index bd58f85..008055f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -fakemachine +/fakemachine .*swp diff --git a/README.md b/README.md index fed5766..c68f022 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# fakemchine - fake a machine +# fakemachine - fake a machine -Creates a vm based on the currently running system. +Creates a virtual machine based on the currently running system. ## Synopsis - fakemachine [OPTIONS] - ``` +fakemachine [options] +fakemachine [--help] +``` + Application Options: +``` -b, --backend=[auto|kvm|uml|qemu] Virtualisation backend to use (default: auto) -v, --volume= volume to mount -i, --image= image to add diff --git a/backend.go b/backend.go index 134ca01..5a4ae2a 100644 --- a/backend.go +++ b/backend.go @@ -43,6 +43,12 @@ func newBackend(name string, m *Machine) (backend, error) { if name == "auto" { for _, backend := range backends { backendName := backend.Name() + + /* The qemu backend is slow, don't allow users to auto-select it */ + if backendName == "qemu" { + continue + } + b, backendErr := newBackend(backendName, m) if backendErr != nil { err = fmt.Errorf("%v, %v", err, backendErr) diff --git a/backend_qemu.go b/backend_qemu.go index 423e1a3..db4425a 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -216,7 +216,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { for _, point := range m.mounts { qemuargs = append(qemuargs, "-virtfs", - fmt.Sprintf("local,mount_tag=%s,path=%s,security_model=none", + fmt.Sprintf("local,mount_tag=%s,path=%s,security_model=none,multidevs=remap", point.label, point.hostDirectory)) } diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index f2865c0..bc5a028 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -18,6 +18,7 @@ type Options struct { CPUs int `short:"c" long:"cpus" description:"Number of CPUs for the fakemachine"` ScratchSize string `short:"s" long:"scratchsize" description:"On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, memory backed scratch space is used"` ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fakemachine"` + Quiet bool `short:"q" long:"quiet" description:"Don't show logs from fakemachine or the backend; only print the command's stdout/stderr"` } var options Options @@ -77,7 +78,9 @@ func SetupImages(m *fakemachine.Machine, options Options) { os.Exit(1) } - fmt.Printf("Exposing %s as %s\n", parts[0], l) + if !options.Quiet { + fmt.Printf("Exposing %s as %s\n", parts[0], l) + } } } @@ -153,6 +156,7 @@ func main() { } m.SetShowBoot(options.ShowBoot) + m.SetQuiet(options.Quiet) SetupVolumes(m, options) SetupImages(m, options) SetupEnviron(m, options) diff --git a/doc/man/create_manpage.sh b/doc/man/create_manpage.sh new file mode 100755 index 0000000..baba7f0 --- /dev/null +++ b/doc/man/create_manpage.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Create a manpage from the README.md + +# Add header +echo '''% fakemachine(1) + +# NAME + +fakemachine - fake a machine +''' > fakemachine.md + +# Add README.md +tail -n +2 ../../README.md >> fakemachine.md + +# Some tweaks to the markdown +# Uppercase titles +sed -i 's/^\(##.*\)$/\U\1/' fakemachine.md + +# Remove double # +sed -i 's/^\##/#/' fakemachine.md + +# Create the manpage +pandoc -s -t man fakemachine.md -o fakemachine.1 + +# Resulting manpage can be browsed with groff: +#groff -man -Tascii fakemachine.1 diff --git a/doc/man/fakemachine.1 b/doc/man/fakemachine.1 new file mode 100644 index 0000000..ef42ab3 --- /dev/null +++ b/doc/man/fakemachine.1 @@ -0,0 +1,50 @@ +.\" Automatically generated by Pandoc 2.17.1.1 +.\" +.\" Define V font for inline verbatim, using C font in formats +.\" that render this, and otherwise B font. +.ie "\f[CB]x\f[]"x" \{\ +. ftr V B +. ftr VI BI +. ftr VB B +. ftr VBI BI +.\} +.el \{\ +. ftr V CR +. ftr VI CI +. ftr VB CB +. ftr VBI CBI +.\} +.TH "fakemachine" "1" "" "" "" +.hy +.SH NAME +.PP +fakemachine - fake a machine +.PP +Creates a virtual machine based on the currently running system. +.SH SYNOPSIS +.IP +.nf +\f[C] +fakemachine [options] +fakemachine [--help] +\f[R] +.fi +.PP +Application Options: +.IP +.nf +\f[C] + -b, --backend=[auto|kvm|uml|qemu] Virtualisation backend to use (default: auto) + -v, --volume= volume to mount + -i, --image= image to add + -e, --environ-var= Environment variables (use -e VARIABLE:VALUE syntax) + -m, --memory= Amount of memory for the fakemachine in megabytes + -c, --cpus= Number of CPUs for the fakemachine + -s, --scratchsize= On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, + memory backed scratch space is used + --show-boot Show boot/console messages from the fakemachine + +Help Options: + -h, --help Show this help message +\f[R] +.fi diff --git a/doc/man/fakemachine.md b/doc/man/fakemachine.md new file mode 100644 index 0000000..d35835e --- /dev/null +++ b/doc/man/fakemachine.md @@ -0,0 +1,31 @@ +% fakemachine(1) + +# NAME + +fakemachine - fake a machine + + +Creates a virtual machine based on the currently running system. + +# SYNOPSIS + +``` +fakemachine [options] +fakemachine [--help] +``` + +Application Options: +``` + -b, --backend=[auto|kvm|uml|qemu] Virtualisation backend to use (default: auto) + -v, --volume= volume to mount + -i, --image= image to add + -e, --environ-var= Environment variables (use -e VARIABLE:VALUE syntax) + -m, --memory= Amount of memory for the fakemachine in megabytes + -c, --cpus= Number of CPUs for the fakemachine + -s, --scratchsize= On-disk scratch space size (with a unit suffix, e.g. 4G); if unset, + memory backed scratch space is used + --show-boot Show boot/console messages from the fakemachine + +Help Options: + -h, --help Show this help message +``` diff --git a/go.mod b/go.mod index 6d0daba..ece4cb5 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/docker/go-units v0.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/klauspost/compress v1.15.3 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/surma/gocpio v1.1.0 - github.com/ulikunitz/xz v0.5.10 + github.com/ulikunitz/xz v0.5.11 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad ) diff --git a/go.sum b/go.sum index b1d9995..f921beb 100644 --- a/go.sum +++ b/go.sum @@ -11,13 +11,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/surma/gocpio v1.1.0 h1:RUWT+VqJ8GSodSv7Oh5xjIxy7r24CV1YvothHFfPxcQ= github.com/surma/gocpio v1.1.0/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9LCVI= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/machine.go b/machine.go index ae23a67..eeb3322 100644 --- a/machine.go +++ b/machine.go @@ -13,12 +13,13 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "runtime" "strconv" "strings" "text/template" - "github.com/go-debos/fakemachine/cpio" + writerhelper "github.com/go-debos/fakemachine/cpio" ) func mergedUsrSystem() bool { @@ -162,6 +163,7 @@ type Machine struct { memory int numcpus int showBoot bool + quiet bool Environ []string scratchsize int64 @@ -272,8 +274,7 @@ if [ $? != 0 ]; then exit fi -echo Running '%[2]s' using '%[1]s' backend -%[2]s +%[1]s echo $? > /run/fakemachine/result ` @@ -294,7 +295,7 @@ Environment=HOME=/root IN_FAKE_MACHINE=yes %[2]s WorkingDirectory=-/scratch ExecStart=/wrapper ExecStopPost=/bin/sync -ExecStopPost=/bin/systemctl poweroff -ff +ExecStopPost=/bin/systemctl poweroff -q -ff Type=idle TTYPath=%[1]s StandardInput=tty-force @@ -446,6 +447,13 @@ func (m *Machine) SetShowBoot(showBoot bool) { m.showBoot = showBoot } +// SetQuiet sets whether fakemachine should print additional information (e.g. +// the command to be ran) or just print the stdout/stderr of the command to be +// ran. +func (m *Machine) SetQuiet(quiet bool) { + m.quiet = quiet +} + // SetScratch sets the size and location of on-disk scratch space to allocate // (sparsely) for /scratch. If not set /scratch will be backed by memory. If // Path is "" then the working directory is used as a default storage location @@ -578,6 +586,25 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) os.Setenv("PATH", os.Getenv("PATH")+":/sbin:/usr/sbin") + /* Sanity check mountpoints */ + for _, v := range m.mounts { + /* Check the directory exists on the host */ + stat, err := os.Stat(v.hostDirectory) + if err != nil || !stat.IsDir() { + return -1, fmt.Errorf("Couldn't mount %s inside machine: expected a directory", v.hostDirectory) + } + + /* Check for whitespace in the machine directory */ + if regexp.MustCompile(`\s`).MatchString(v.machineDirectory) { + return -1, fmt.Errorf("Couldn't mount %s inside machine: machine directory (%s) contains whitespace", v.hostDirectory, v.machineDirectory) + } + + /* Check for whitespace in the label */ + if regexp.MustCompile(`\s`).MatchString(v.label) { + return -1, fmt.Errorf("Couldn't mount %s inside machine: label (%s) contains whitespace", v.hostDirectory, v.label) + } + } + tmpdir, err := ioutil.TempDir("", "fakemachine-") if err != nil { return -1, err @@ -662,6 +689,11 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) return -1, err } + /* Ensure systemd-resolved is available */ + if _, err := os.Stat("/lib/systemd/systemd-resolved"); err != nil { + return -1, err + } + /* Amd64 dynamic linker */ err = w.CopyFile("/lib64/ld-linux-x86-64.so.2") if err != nil { @@ -765,7 +797,7 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) } err = w.WriteFile("/wrapper", - fmt.Sprintf(commandWrapper, backend.Name(), command), 0755) + fmt.Sprintf(commandWrapper, command), 0755) if err != nil { return -1, err } @@ -795,6 +827,10 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) w.Close() f.Close() + if !m.quiet { + fmt.Printf("Running %s using %s backend\n", command, backend.Name()) + } + success, err := backend.Start() if !success || err != nil { return -1, fmt.Errorf("error starting %s backend: %v", backend.Name(), err) diff --git a/machine_test.go b/machine_test.go index 804a700..bcdc6cd 100644 --- a/machine_test.go +++ b/machine_test.go @@ -3,11 +3,12 @@ package fakemachine import ( "bufio" "flag" - "github.com/stretchr/testify/assert" "io" "os" "strings" "testing" + + "github.com/stretchr/testify/require" ) var backendName string @@ -18,13 +19,13 @@ func init() { func CreateMachine(t *testing.T) *Machine { machine, err := NewMachineWithBackend(backendName) - assert.Nil(t, err) + require.Nil(t, err) machine.SetNumCPUs(2) return machine } -func TestSuccessfullCommand(t *testing.T) { +func TestSuccessfulCommand(t *testing.T) { t.Parallel() m := CreateMachine(t) @@ -50,7 +51,7 @@ func TestImage(t *testing.T) { m := CreateMachine(t) _, err := m.CreateImage("test.img", 1024*1024) - assert.Nil(t, err) + require.Nil(t, err) exitcode, _ := m.Run("test -b /dev/disk/by-fakemachine-label/fakedisk-0") if exitcode != 0 { @@ -60,21 +61,21 @@ func TestImage(t *testing.T) { func AssertMount(t *testing.T, mountpoint, fstype string) { m, err := os.Open("/proc/self/mounts") - assert.Nil(t, err) + require.Nil(t, err) mtab := bufio.NewReader(m) for { line, err := mtab.ReadString('\n') if err == io.EOF { - assert.Fail(t, "mountpoint not found") + require.Fail(t, "mountpoint not found") break } - assert.Nil(t, err) + require.Nil(t, err) fields := strings.Fields(line) if fields[1] == mountpoint { - assert.Equal(t, fields[2], fstype) + require.Equal(t, fields[2], fstype) return } } @@ -131,7 +132,7 @@ fi exitcode, _ := m.Run(command) if exitcode != 0 { - t.Fatalf("Test for tmpfs mount on scratch failed with %d", exitcode) + t.Fatalf("Test for set memory failed with %d", exitcode) } } @@ -156,31 +157,63 @@ func TestImageLabel(t *testing.T) { if InMachine() { t.Log("Running in the machine") devices := flag.Args() - assert.Equal(t, len(devices), 2, "Only expected two devices") + require.Equal(t, len(devices), 2, "Only expected two devices") autolabel := devices[0] labeled := devices[1] info, err := os.Stat(autolabel) - assert.Nil(t, err) - assert.Equal(t, info.Mode()&os.ModeType, os.ModeDevice, "Expected a device") + require.Nil(t, err) + require.Equal(t, info.Mode()&os.ModeType, os.ModeDevice, "Expected a device") info, err = os.Stat(labeled) - assert.Nil(t, err) - assert.Equal(t, info.Mode()&os.ModeType, os.ModeDevice, "Expected a device") + require.Nil(t, err) + require.Equal(t, info.Mode()&os.ModeType, os.ModeDevice, "Expected a device") return } m := CreateMachine(t) autolabel, err := m.CreateImage("test-autolabel.img", 1024*1024) - assert.Nil(t, err) + require.Nil(t, err) labeled, err := m.CreateImageWithLabel("test-labeled.img", 1024*1024, "test-labeled") - assert.Nil(t, err) + require.Nil(t, err) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestImageLabel", autolabel, labeled}) if exitcode != 0 { t.Fatalf("Test for images in the machine failed failed with %d", exitcode) } } + +func TestVolumes(t *testing.T) { + t.Parallel() + if InMachine() { + t.Log("Running in the machine") + return + } + + /* Try to mount a non-existent file into the machine */ + m := CreateMachine(t) + m.AddVolume("random_directory_never_exists") + + exitcode, err := m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + require.Equal(t, exitcode, -1) + require.Error(t, err) + + /* Try to mount a device file into the machine */ + m = CreateMachine(t) + m.AddVolume("/dev/zero") + + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + require.Equal(t, exitcode, -1) + require.Error(t, err) + + /* Try to mount a volume with whitespace into the machine */ + m = CreateMachine(t) + m.AddVolumeAt("/dev", "/dev ices") + + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + require.Equal(t, exitcode, -1) + require.Error(t, err) +} From d9c5f0ddd607e19dfe4a909e485cda494770deab Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 13:41:24 +0000 Subject: [PATCH 29/52] Bump debhelper-compat to 13 Signed-off-by: Christopher Obbard --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 952a9fe..413cdf7 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ Uploaders: Christopher Obbard , Héctor Orón Martínez , Build-Depends: - debhelper-compat (= 12), + debhelper-compat (= 13), dh-golang, golang-any, golang-github-docker-go-units-dev, From 06bdcb0720ed29d76b44c7c074f8b36738604eec Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 15:53:47 +0000 Subject: [PATCH 30/52] Ship manpage from upstream tarball Signed-off-by: Christopher Obbard --- debian/manpages | 1 + 1 file changed, 1 insertion(+) create mode 100644 debian/manpages diff --git a/debian/manpages b/debian/manpages new file mode 100644 index 0000000..d1cef23 --- /dev/null +++ b/debian/manpages @@ -0,0 +1 @@ +doc/man/fakemachine.1 From 34e17d71a8410fd2f6ab99b3fe92fc169489a2cc Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 15:56:48 +0000 Subject: [PATCH 31/52] lintian: Don't warn about compressed test data Upstream ships compressed test data which may be duplicated, don't warn about it. Signed-off-by: Christopher Obbard --- .../golang-github-go-debos-fakemachine-dev.lintian-overrides | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 debian/golang-github-go-debos-fakemachine-dev.lintian-overrides diff --git a/debian/golang-github-go-debos-fakemachine-dev.lintian-overrides b/debian/golang-github-go-debos-fakemachine-dev.lintian-overrides new file mode 100644 index 0000000..88f1aeb --- /dev/null +++ b/debian/golang-github-go-debos-fakemachine-dev.lintian-overrides @@ -0,0 +1,3 @@ +# compressed test data may be duplicated +compressed-duplicate [usr/share/gocode/src/github.com/go-debos/fakemachine/testdata/test.gz] +compressed-duplicate [usr/share/gocode/src/github.com/go-debos/fakemachine/testdata/test.xz] From 59e220ec6907024e7ca805e31ae150ef73a23877 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 16:04:53 +0000 Subject: [PATCH 32/52] Improve package descriptions Signed-off-by: Christopher Obbard --- debian/control | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 413cdf7..d171bf5 100644 --- a/debian/control +++ b/debian/control @@ -38,8 +38,10 @@ Recommends: linux-image-amd64, systemd-resolved, Suggests: libslirp-helper, user-mode-linux -Description: create and spawn virtual machines +Description: Create and spawn virtual machines (program) Create and spawn virtual machines based on the currently running system. + . + This package contains the fakemachine program. Package: golang-github-go-debos-fakemachine-dev Architecture: amd64 @@ -54,6 +56,7 @@ Depends: golang-golang-x-sys-dev, ${misc:Depends}, ${shlibs:Depends}, -Description: create and spawn virtual machines +Description: create and spawn virtual machines (library) Create and spawn virtual machines based on the currently running system. - (development libraries) + . + This package contains the library. From 48ade9ee31d0fcb4c4997a2b6009ce7d06f60c1f Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 16:07:49 +0000 Subject: [PATCH 33/52] Update changelog for 0.0.4-1 release --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index d60c8f5..3a0e11e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +golang-github-go-debos-fakemachine (0.0.4-1) unstable; urgency=medium + + * New upstream version 0.0.4 + * Bump debhelper-compat to 13 + * Ship manpage from upstream tarball + * lintian: Don't warn about compressed test data + * Improve package descriptions + + -- Christopher Obbard Wed, 08 Feb 2023 16:07:43 +0000 + golang-github-go-debos-fakemachine (0.0.3-3) unstable; urgency=medium * Bump Standards-Version to 4.6.1 From 3b6063c2434507e675d1278e601283e707fa182c Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 8 Feb 2023 16:27:33 +0000 Subject: [PATCH 34/52] Bump Standards-Version to 4.6.2 Signed-off-by: Christopher Obbard --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index d171bf5..f13f459 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Build-Depends: golang-github-ulikunitz-xz-dev, golang-go-flags-dev, golang-golang-x-sys-dev, -Standards-Version: 4.6.1 +Standards-Version: 4.6.2 Rules-Requires-Root: no Homepage: https://github.com/go-debos/fakemachine Vcs-Browser: https://salsa.debian.org/go-team/packages/golang-github-go-debos-fakemachine From 50fff989059adc4aa28868ab251608c24d15f04a Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Mon, 24 Jul 2023 16:15:46 +0100 Subject: [PATCH 35/52] New upstream version 0.0.5 --- .github/workflows/ci.yml | 20 +++++++------ .golangci.yml | 1 + backend.go | 6 ++-- backend_qemu.go | 48 ++++++++++++++++++++++++++---- backend_uml.go | 13 ++++++--- bors.toml | 2 -- cmd/fakemachine/main.go | 4 +-- cpio/writerhelper.go | 2 +- decompressors_test.go | 6 ++-- machine.go | 63 ++++++++++++++++++++++++++++++---------- 10 files changed, 120 insertions(+), 45 deletions(-) delete mode 100644 bors.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89af7ff..d1c4c2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v4 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -46,9 +46,9 @@ jobs: # functionality without hardware acceleration since the majority of code # is shared between the qemu and kvm backends. # See https://github.com/actions/runner-images/issues/183 - # + # # For Arch Linux uml is not yet supported, so only test under qemu there. - os: [bullseye, bookworm] + os: [bullseye, bookworm, trixie] backend: [qemu, uml] include: - os: arch @@ -79,15 +79,17 @@ jobs: - name: Ensure no tests were skipped run: "! grep -q SKIP test.out" - # Job to key the bors success status against - bors: - name: bors - if: success() + # Job to key success status against + allgreen: + name: allgreen + if: always() needs: - golangci - man-page - test runs-on: ubuntu-latest steps: - - name: Mark the job as a success - run: exit 0 + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.golangci.yml b/.golangci.yml index 72f3064..6b63b2b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,5 @@ linters: enable: - gofmt + - stylecheck - whitespace diff --git a/backend.go b/backend.go index 5a4ae2a..478911a 100644 --- a/backend.go +++ b/backend.go @@ -1,5 +1,5 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux +// +build linux package fakemachine @@ -71,7 +71,7 @@ func newBackend(name string, m *Machine) (backend, error) { // check backend is supported if supported, err := b.Supported(); !supported { - return nil, fmt.Errorf("%s backend not supported: %v", name, err) + return nil, fmt.Errorf("%s backend not supported: %w", name, err) } return b, nil diff --git a/backend_qemu.go b/backend_qemu.go index db4425a..840e7bd 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -1,5 +1,4 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux && (arm64 || amd64) package fakemachine @@ -35,8 +34,40 @@ func (b qemuBackend) Supported() (bool, error) { return true, nil } +type qemuMachine struct { + binary string + console string + machine string + /* Cpu to use for qemu backend if the architecture doesn't have a good default */ + qemuCPU string +} + +var qemuMachines = map[Arch]qemuMachine{ + Amd64: { + binary: "qemu-system-x86_64", + console: "ttyS0", + machine: "pc", + }, + Arm64: { + binary: "qemu-system-aarch64", + console: "ttyAMA0", + machine: "virt", + /* The default cpu is a 32 bit one, which isn't very usefull + * for 64 bit arm. There is no cpu setting for "minimal" 64 + * bit linux capable processor. The only generic setting + * is "max", but that can be very slow to emulate. So pick + * a specific small cortex-a processor instead */ + qemuCPU: "cortex-a53", + }, +} + func (b qemuBackend) QemuPath() (string, error) { - return exec.LookPath("qemu-system-x86_64") + machine, ok := qemuMachines[b.machine.arch] + if !ok { + return "", fmt.Errorf("unsupported arch for qemu: %s", b.machine.arch) + } + + return exec.LookPath(machine.binary) } func (b qemuBackend) KernelRelease() (string, error) { @@ -68,7 +99,7 @@ func (b qemuBackend) KernelRelease() (string, error) { } } - return "", fmt.Errorf("No kernel found") + return "", fmt.Errorf("kernel not found") } func (b qemuBackend) KernelPath() (string, error) { @@ -166,6 +197,7 @@ func (b qemuBackend) Start() (bool, error) { func (b qemuBackend) StartQemu(kvm bool) (bool, error) { m := b.machine + qemuMachine := qemuMachines[m.arch] kernelPath, err := b.KernelPath() if err != nil { @@ -173,7 +205,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { } memory := fmt.Sprintf("%d", m.memory) numcpus := fmt.Sprintf("%d", m.numcpus) - qemuargs := []string{"qemu-system-x86_64", + qemuargs := []string{qemuMachine.binary, "-smp", numcpus, "-m", memory, "-kernel", kernelPath, @@ -185,9 +217,13 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { qemuargs = append(qemuargs, "-cpu", "host", "-enable-kvm") + } else if qemuMachine.qemuCPU != "" { + qemuargs = append(qemuargs, "-cpu", qemuMachine.qemuCPU) } - kernelargs := []string{"console=ttyS0", "panic=-1", + qemuargs = append(qemuargs, "-machine", qemuMachine.machine) + console := fmt.Sprintf("console=%s", qemuMachine.console) + kernelargs := []string{console, "panic=-1", "systemd.unit=fakemachine.service"} if m.showBoot { diff --git a/backend_uml.go b/backend_uml.go index 3f0bc6e..fa5a589 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -1,5 +1,5 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux +// +build linux package fakemachine @@ -27,6 +27,11 @@ func (b umlBackend) Name() string { } func (b umlBackend) Supported() (bool, error) { + // only support amd64 + if b.machine.arch != Amd64 { + return false, fmt.Errorf("unsupported arch: %s", b.machine.arch) + } + // check the kernel exists if _, err := b.KernelPath(); err != nil { return false, err @@ -45,7 +50,7 @@ func (b umlBackend) Supported() (bool, error) { } func (b umlBackend) KernelRelease() (string, error) { - return "", errors.New("Not implemented") + return "", errors.New("not implemented") } func (b umlBackend) KernelPath() (string, error) { @@ -162,7 +167,7 @@ func (b umlBackend) Start() (bool, error) { // one of the sockets will be attached to the slirp-helper slirpHelperSocket := os.NewFile(uintptr(netSocketpair[0]), "") if slirpHelperSocket == nil { - return false, fmt.Errorf("Creation of slirpHelperSocket failed") + return false, fmt.Errorf("creation of slirpHelperSocket failed") } defer slirpHelperSocket.Close() diff --git a/bors.toml b/bors.toml deleted file mode 100644 index 1db5825..0000000 --- a/bors.toml +++ /dev/null @@ -1,2 +0,0 @@ -status = [ "bors" ] -delete_merged_branches = true diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index bc5a028..6e69926 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -91,7 +91,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { // These are the environment variables that will be detected on the // host and propagated to fakemachine. These are listed lower case, but // they are detected and configured in both lower case and upper case. - var environ_vars = [...]string{ + var environVars = [...]string{ "http_proxy", "https_proxy", "ftp_proxy", @@ -101,7 +101,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { } // First add variables from host - for _, e := range environ_vars { + for _, e := range environVars { lowerVar := strings.ToLower(e) // lowercase not really needed lowerVal := os.Getenv(lowerVar) if lowerVal != "" { diff --git a/cpio/writerhelper.go b/cpio/writerhelper.go index 72cce4d..eeef5d8 100644 --- a/cpio/writerhelper.go +++ b/cpio/writerhelper.go @@ -197,7 +197,7 @@ func (w *WriterHelper) CopyFileTo(src, dst string) error { f, err := os.Open(src) if err != nil { - return fmt.Errorf("open failed: %s - %v", src, err) + return fmt.Errorf("open failed: %s - %w", src, err) } defer f.Close() diff --git a/decompressors_test.go b/decompressors_test.go index 8b1a77e..cd5aab8 100644 --- a/decompressors_test.go +++ b/decompressors_test.go @@ -58,14 +58,14 @@ func decompressorTest(t *testing.T, file, suffix string, d writerhelper.Transfor return } - check_f, err := os.Open(path.Join("testdata", file)) + checkFile, err := os.Open(path.Join("testdata", file)) if err != nil { t.Errorf("Unable to open check data: %s", err) return } - defer check_f.Close() + defer checkFile.Close() - err = checkStreamsMatch(t, output, check_f) + err = checkStreamsMatch(t, output, checkFile) if err != nil { t.Errorf("Failed to compare streams: %s", err) return diff --git a/machine.go b/machine.go index eeb3322..1fdfc35 100644 --- a/machine.go +++ b/machine.go @@ -1,5 +1,4 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux && (arm64 || amd64) package fakemachine @@ -68,6 +67,16 @@ func getModDepends(modname string, kernelRelease string) []string { } } + // Busybox expects a full dependency list for each module rather than just + // direct dependencies, so recurse the module dependency tree: + // https://github.com/mirror/busybox/blob/1dd2685dcc735496d7adde87ac60b9434ed4a04c/modutils/modprobe.c#L46-L49 + var sublist []string + for _, mod := range modlist { + sublist = append(sublist, getModDepends(mod, kernelRelease)...) + } + + modlist = append(modlist, sublist...) + return modlist } @@ -82,7 +91,7 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi release, _ := m.backend.KernelRelease() modpath := getModPath(modname, release) if modpath == "" { - return errors.New("Modules path couldn't be determined") + return errors.New("modules path couldn't be determined") } if modpath == "(builtin)" || copiedModules[modname] { @@ -114,7 +123,7 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi } } if !found { - return errors.New("Module extension/suffix unknown") + return errors.New("module extension/suffix unknown") } copiedModules[modname] = true @@ -143,6 +152,23 @@ func realDir(path string) (string, error) { return filepath.Dir(p), nil } +type Arch string + +const ( + Amd64 Arch = "amd64" + Arm64 Arch = "arm64" +) + +var archMap = map[string]Arch{ + "amd64": Amd64, + "arm64": Arm64, +} + +var archDynamicLinker = map[Arch]string{ + Amd64: "/lib64/ld-linux-x86-64.so.2", + Arm64: "/lib/ld-linux-aarch64.so.1", +} + type mountPoint struct { hostDirectory string machineDirectory string @@ -156,6 +182,7 @@ type image struct { } type Machine struct { + arch Arch backend backend mounts []mountPoint count int @@ -183,6 +210,11 @@ func NewMachineWithBackend(backendName string) (*Machine, error) { var err error m := &Machine{memory: 2048, numcpus: runtime.NumCPU()} + var ok bool + if m.arch, ok = archMap[runtime.GOARCH]; !ok { + return nil, fmt.Errorf("unsupported arch %s", runtime.GOARCH) + } + m.backend, err = newBackend(backendName, m) if err != nil { return nil, err @@ -395,12 +427,12 @@ func (m *Machine) CreateImageWithLabel(path string, size int64, label string) (s } if len(label) >= 20 { - return "", fmt.Errorf("Label '%s' too long; cannot be more then 20 characters", label) + return "", fmt.Errorf("label '%s' too long; cannot be more then 20 characters", label) } for _, image := range m.images { if image.label == label { - return "", fmt.Errorf("Label '%s' already exists", label) + return "", fmt.Errorf("label '%s' already exists", label) } } @@ -496,7 +528,7 @@ func stripCompressionSuffix(module string) (string, error) { return strings.TrimSuffix(module, suffix) + ".ko", nil } } - return "", errors.New("Module extension/suffix unknown") + return "", errors.New("module extension/suffix unknown") } func (m *Machine) generateModulesDep(w *writerhelper.WriterHelper, moddir string, modules map[string]bool) error { @@ -591,17 +623,17 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) /* Check the directory exists on the host */ stat, err := os.Stat(v.hostDirectory) if err != nil || !stat.IsDir() { - return -1, fmt.Errorf("Couldn't mount %s inside machine: expected a directory", v.hostDirectory) + return -1, fmt.Errorf("couldn't mount %s inside machine: expected a directory", v.hostDirectory) } /* Check for whitespace in the machine directory */ if regexp.MustCompile(`\s`).MatchString(v.machineDirectory) { - return -1, fmt.Errorf("Couldn't mount %s inside machine: machine directory (%s) contains whitespace", v.hostDirectory, v.machineDirectory) + return -1, fmt.Errorf("couldn't mount %s inside machine: machine directory (%s) contains whitespace", v.hostDirectory, v.machineDirectory) } /* Check for whitespace in the label */ if regexp.MustCompile(`\s`).MatchString(v.label) { - return -1, fmt.Errorf("Couldn't mount %s inside machine: label (%s) contains whitespace", v.hostDirectory, v.label) + return -1, fmt.Errorf("couldn't mount %s inside machine: label (%s) contains whitespace", v.hostDirectory, v.label) } } @@ -659,6 +691,7 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) {Target: "/usr/sbin", Link: "/sbin", Perm: 0755}, {Target: "/usr/bin", Link: "/bin", Perm: 0755}, {Target: "/usr/lib", Link: "/lib", Perm: 0755}, + {Target: "/usr/lib64", Link: "/lib64", Perm: 0755}, }) if err != nil { return -1, err @@ -694,14 +727,14 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) return -1, err } - /* Amd64 dynamic linker */ - err = w.CopyFile("/lib64/ld-linux-x86-64.so.2") + dynamicLinker := archDynamicLinker[m.arch] + err = w.CopyFile(prefix + dynamicLinker) if err != nil { return -1, err } /* C libraries */ - libraryDir, err := realDir("/lib64/ld-linux-x86-64.so.2") + libraryDir, err := realDir(dynamicLinker) if err != nil { return -1, err } @@ -833,7 +866,7 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) success, err := backend.Start() if !success || err != nil { - return -1, fmt.Errorf("error starting %s backend: %v", backend.Name(), err) + return -1, fmt.Errorf("error starting %s backend: %w", backend.Name(), err) } result, err := os.Open(path.Join(tmpdir, "result")) @@ -867,7 +900,7 @@ func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { executable, err := exec.LookPath(os.Args[0]) if err != nil { - return -1, fmt.Errorf("Failed to find executable: %v\n", err) + return -1, fmt.Errorf("failed to find executable: %w", err) } return m.startup(command, [][2]string{{executable, name}}) From 63a97aad8c578a8b1ebf86b628d780cdcbd3e34f Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Mon, 24 Jul 2023 16:15:46 +0100 Subject: [PATCH 36/52] New upstream version 0.0.5 --- .github/workflows/ci.yml | 20 +++++++------ .golangci.yml | 1 + backend.go | 6 ++-- backend_qemu.go | 48 ++++++++++++++++++++++++++---- backend_uml.go | 13 ++++++--- bors.toml | 2 -- cmd/fakemachine/main.go | 4 +-- cpio/writerhelper.go | 2 +- decompressors_test.go | 6 ++-- machine.go | 63 ++++++++++++++++++++++++++++++---------- 10 files changed, 120 insertions(+), 45 deletions(-) delete mode 100644 bors.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89af7ff..d1c4c2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v4 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -46,9 +46,9 @@ jobs: # functionality without hardware acceleration since the majority of code # is shared between the qemu and kvm backends. # See https://github.com/actions/runner-images/issues/183 - # + # # For Arch Linux uml is not yet supported, so only test under qemu there. - os: [bullseye, bookworm] + os: [bullseye, bookworm, trixie] backend: [qemu, uml] include: - os: arch @@ -79,15 +79,17 @@ jobs: - name: Ensure no tests were skipped run: "! grep -q SKIP test.out" - # Job to key the bors success status against - bors: - name: bors - if: success() + # Job to key success status against + allgreen: + name: allgreen + if: always() needs: - golangci - man-page - test runs-on: ubuntu-latest steps: - - name: Mark the job as a success - run: exit 0 + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.golangci.yml b/.golangci.yml index 72f3064..6b63b2b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,5 @@ linters: enable: - gofmt + - stylecheck - whitespace diff --git a/backend.go b/backend.go index 5a4ae2a..478911a 100644 --- a/backend.go +++ b/backend.go @@ -1,5 +1,5 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux +// +build linux package fakemachine @@ -71,7 +71,7 @@ func newBackend(name string, m *Machine) (backend, error) { // check backend is supported if supported, err := b.Supported(); !supported { - return nil, fmt.Errorf("%s backend not supported: %v", name, err) + return nil, fmt.Errorf("%s backend not supported: %w", name, err) } return b, nil diff --git a/backend_qemu.go b/backend_qemu.go index db4425a..840e7bd 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -1,5 +1,4 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux && (arm64 || amd64) package fakemachine @@ -35,8 +34,40 @@ func (b qemuBackend) Supported() (bool, error) { return true, nil } +type qemuMachine struct { + binary string + console string + machine string + /* Cpu to use for qemu backend if the architecture doesn't have a good default */ + qemuCPU string +} + +var qemuMachines = map[Arch]qemuMachine{ + Amd64: { + binary: "qemu-system-x86_64", + console: "ttyS0", + machine: "pc", + }, + Arm64: { + binary: "qemu-system-aarch64", + console: "ttyAMA0", + machine: "virt", + /* The default cpu is a 32 bit one, which isn't very usefull + * for 64 bit arm. There is no cpu setting for "minimal" 64 + * bit linux capable processor. The only generic setting + * is "max", but that can be very slow to emulate. So pick + * a specific small cortex-a processor instead */ + qemuCPU: "cortex-a53", + }, +} + func (b qemuBackend) QemuPath() (string, error) { - return exec.LookPath("qemu-system-x86_64") + machine, ok := qemuMachines[b.machine.arch] + if !ok { + return "", fmt.Errorf("unsupported arch for qemu: %s", b.machine.arch) + } + + return exec.LookPath(machine.binary) } func (b qemuBackend) KernelRelease() (string, error) { @@ -68,7 +99,7 @@ func (b qemuBackend) KernelRelease() (string, error) { } } - return "", fmt.Errorf("No kernel found") + return "", fmt.Errorf("kernel not found") } func (b qemuBackend) KernelPath() (string, error) { @@ -166,6 +197,7 @@ func (b qemuBackend) Start() (bool, error) { func (b qemuBackend) StartQemu(kvm bool) (bool, error) { m := b.machine + qemuMachine := qemuMachines[m.arch] kernelPath, err := b.KernelPath() if err != nil { @@ -173,7 +205,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { } memory := fmt.Sprintf("%d", m.memory) numcpus := fmt.Sprintf("%d", m.numcpus) - qemuargs := []string{"qemu-system-x86_64", + qemuargs := []string{qemuMachine.binary, "-smp", numcpus, "-m", memory, "-kernel", kernelPath, @@ -185,9 +217,13 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { qemuargs = append(qemuargs, "-cpu", "host", "-enable-kvm") + } else if qemuMachine.qemuCPU != "" { + qemuargs = append(qemuargs, "-cpu", qemuMachine.qemuCPU) } - kernelargs := []string{"console=ttyS0", "panic=-1", + qemuargs = append(qemuargs, "-machine", qemuMachine.machine) + console := fmt.Sprintf("console=%s", qemuMachine.console) + kernelargs := []string{console, "panic=-1", "systemd.unit=fakemachine.service"} if m.showBoot { diff --git a/backend_uml.go b/backend_uml.go index 3f0bc6e..fa5a589 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -1,5 +1,5 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux +// +build linux package fakemachine @@ -27,6 +27,11 @@ func (b umlBackend) Name() string { } func (b umlBackend) Supported() (bool, error) { + // only support amd64 + if b.machine.arch != Amd64 { + return false, fmt.Errorf("unsupported arch: %s", b.machine.arch) + } + // check the kernel exists if _, err := b.KernelPath(); err != nil { return false, err @@ -45,7 +50,7 @@ func (b umlBackend) Supported() (bool, error) { } func (b umlBackend) KernelRelease() (string, error) { - return "", errors.New("Not implemented") + return "", errors.New("not implemented") } func (b umlBackend) KernelPath() (string, error) { @@ -162,7 +167,7 @@ func (b umlBackend) Start() (bool, error) { // one of the sockets will be attached to the slirp-helper slirpHelperSocket := os.NewFile(uintptr(netSocketpair[0]), "") if slirpHelperSocket == nil { - return false, fmt.Errorf("Creation of slirpHelperSocket failed") + return false, fmt.Errorf("creation of slirpHelperSocket failed") } defer slirpHelperSocket.Close() diff --git a/bors.toml b/bors.toml deleted file mode 100644 index 1db5825..0000000 --- a/bors.toml +++ /dev/null @@ -1,2 +0,0 @@ -status = [ "bors" ] -delete_merged_branches = true diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index bc5a028..6e69926 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -91,7 +91,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { // These are the environment variables that will be detected on the // host and propagated to fakemachine. These are listed lower case, but // they are detected and configured in both lower case and upper case. - var environ_vars = [...]string{ + var environVars = [...]string{ "http_proxy", "https_proxy", "ftp_proxy", @@ -101,7 +101,7 @@ func SetupEnviron(m *fakemachine.Machine, options Options) { } // First add variables from host - for _, e := range environ_vars { + for _, e := range environVars { lowerVar := strings.ToLower(e) // lowercase not really needed lowerVal := os.Getenv(lowerVar) if lowerVal != "" { diff --git a/cpio/writerhelper.go b/cpio/writerhelper.go index 72cce4d..eeef5d8 100644 --- a/cpio/writerhelper.go +++ b/cpio/writerhelper.go @@ -197,7 +197,7 @@ func (w *WriterHelper) CopyFileTo(src, dst string) error { f, err := os.Open(src) if err != nil { - return fmt.Errorf("open failed: %s - %v", src, err) + return fmt.Errorf("open failed: %s - %w", src, err) } defer f.Close() diff --git a/decompressors_test.go b/decompressors_test.go index 8b1a77e..cd5aab8 100644 --- a/decompressors_test.go +++ b/decompressors_test.go @@ -58,14 +58,14 @@ func decompressorTest(t *testing.T, file, suffix string, d writerhelper.Transfor return } - check_f, err := os.Open(path.Join("testdata", file)) + checkFile, err := os.Open(path.Join("testdata", file)) if err != nil { t.Errorf("Unable to open check data: %s", err) return } - defer check_f.Close() + defer checkFile.Close() - err = checkStreamsMatch(t, output, check_f) + err = checkStreamsMatch(t, output, checkFile) if err != nil { t.Errorf("Failed to compare streams: %s", err) return diff --git a/machine.go b/machine.go index eeb3322..1fdfc35 100644 --- a/machine.go +++ b/machine.go @@ -1,5 +1,4 @@ -//go:build linux && amd64 -// +build linux,amd64 +//go:build linux && (arm64 || amd64) package fakemachine @@ -68,6 +67,16 @@ func getModDepends(modname string, kernelRelease string) []string { } } + // Busybox expects a full dependency list for each module rather than just + // direct dependencies, so recurse the module dependency tree: + // https://github.com/mirror/busybox/blob/1dd2685dcc735496d7adde87ac60b9434ed4a04c/modutils/modprobe.c#L46-L49 + var sublist []string + for _, mod := range modlist { + sublist = append(sublist, getModDepends(mod, kernelRelease)...) + } + + modlist = append(modlist, sublist...) + return modlist } @@ -82,7 +91,7 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi release, _ := m.backend.KernelRelease() modpath := getModPath(modname, release) if modpath == "" { - return errors.New("Modules path couldn't be determined") + return errors.New("modules path couldn't be determined") } if modpath == "(builtin)" || copiedModules[modname] { @@ -114,7 +123,7 @@ func (m *Machine) copyModules(w *writerhelper.WriterHelper, modname string, copi } } if !found { - return errors.New("Module extension/suffix unknown") + return errors.New("module extension/suffix unknown") } copiedModules[modname] = true @@ -143,6 +152,23 @@ func realDir(path string) (string, error) { return filepath.Dir(p), nil } +type Arch string + +const ( + Amd64 Arch = "amd64" + Arm64 Arch = "arm64" +) + +var archMap = map[string]Arch{ + "amd64": Amd64, + "arm64": Arm64, +} + +var archDynamicLinker = map[Arch]string{ + Amd64: "/lib64/ld-linux-x86-64.so.2", + Arm64: "/lib/ld-linux-aarch64.so.1", +} + type mountPoint struct { hostDirectory string machineDirectory string @@ -156,6 +182,7 @@ type image struct { } type Machine struct { + arch Arch backend backend mounts []mountPoint count int @@ -183,6 +210,11 @@ func NewMachineWithBackend(backendName string) (*Machine, error) { var err error m := &Machine{memory: 2048, numcpus: runtime.NumCPU()} + var ok bool + if m.arch, ok = archMap[runtime.GOARCH]; !ok { + return nil, fmt.Errorf("unsupported arch %s", runtime.GOARCH) + } + m.backend, err = newBackend(backendName, m) if err != nil { return nil, err @@ -395,12 +427,12 @@ func (m *Machine) CreateImageWithLabel(path string, size int64, label string) (s } if len(label) >= 20 { - return "", fmt.Errorf("Label '%s' too long; cannot be more then 20 characters", label) + return "", fmt.Errorf("label '%s' too long; cannot be more then 20 characters", label) } for _, image := range m.images { if image.label == label { - return "", fmt.Errorf("Label '%s' already exists", label) + return "", fmt.Errorf("label '%s' already exists", label) } } @@ -496,7 +528,7 @@ func stripCompressionSuffix(module string) (string, error) { return strings.TrimSuffix(module, suffix) + ".ko", nil } } - return "", errors.New("Module extension/suffix unknown") + return "", errors.New("module extension/suffix unknown") } func (m *Machine) generateModulesDep(w *writerhelper.WriterHelper, moddir string, modules map[string]bool) error { @@ -591,17 +623,17 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) /* Check the directory exists on the host */ stat, err := os.Stat(v.hostDirectory) if err != nil || !stat.IsDir() { - return -1, fmt.Errorf("Couldn't mount %s inside machine: expected a directory", v.hostDirectory) + return -1, fmt.Errorf("couldn't mount %s inside machine: expected a directory", v.hostDirectory) } /* Check for whitespace in the machine directory */ if regexp.MustCompile(`\s`).MatchString(v.machineDirectory) { - return -1, fmt.Errorf("Couldn't mount %s inside machine: machine directory (%s) contains whitespace", v.hostDirectory, v.machineDirectory) + return -1, fmt.Errorf("couldn't mount %s inside machine: machine directory (%s) contains whitespace", v.hostDirectory, v.machineDirectory) } /* Check for whitespace in the label */ if regexp.MustCompile(`\s`).MatchString(v.label) { - return -1, fmt.Errorf("Couldn't mount %s inside machine: label (%s) contains whitespace", v.hostDirectory, v.label) + return -1, fmt.Errorf("couldn't mount %s inside machine: label (%s) contains whitespace", v.hostDirectory, v.label) } } @@ -659,6 +691,7 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) {Target: "/usr/sbin", Link: "/sbin", Perm: 0755}, {Target: "/usr/bin", Link: "/bin", Perm: 0755}, {Target: "/usr/lib", Link: "/lib", Perm: 0755}, + {Target: "/usr/lib64", Link: "/lib64", Perm: 0755}, }) if err != nil { return -1, err @@ -694,14 +727,14 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) return -1, err } - /* Amd64 dynamic linker */ - err = w.CopyFile("/lib64/ld-linux-x86-64.so.2") + dynamicLinker := archDynamicLinker[m.arch] + err = w.CopyFile(prefix + dynamicLinker) if err != nil { return -1, err } /* C libraries */ - libraryDir, err := realDir("/lib64/ld-linux-x86-64.so.2") + libraryDir, err := realDir(dynamicLinker) if err != nil { return -1, err } @@ -833,7 +866,7 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) success, err := backend.Start() if !success || err != nil { - return -1, fmt.Errorf("error starting %s backend: %v", backend.Name(), err) + return -1, fmt.Errorf("error starting %s backend: %w", backend.Name(), err) } result, err := os.Open(path.Join(tmpdir, "result")) @@ -867,7 +900,7 @@ func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { executable, err := exec.LookPath(os.Args[0]) if err != nil { - return -1, fmt.Errorf("Failed to find executable: %v\n", err) + return -1, fmt.Errorf("failed to find executable: %w", err) } return m.startup(command, [][2]string{{executable, name}}) From 3b4548bfcb6a31be2850efc7bf5af2ef36a3a4fb Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Mon, 24 Jul 2023 16:23:01 +0100 Subject: [PATCH 37/52] Build fakemachine for arm64 Signed-off-by: Christopher Obbard --- debian/control | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index f13f459..d5e1fe6 100644 --- a/debian/control +++ b/debian/control @@ -25,17 +25,19 @@ Vcs-Git: https://salsa.debian.org/go-team/packages/golang-github-go-debos-fakema XS-Go-Import-Path: github.com/go-debos/fakemachine Package: fakemachine -Architecture: amd64 +Architecture: amd64 arm64 Built-Using: ${misc:Built-Using} Depends: busybox | busybox-static, - qemu-system-x86, + qemu-system-x86 [amd64], + qemu-system-arm [arm64], systemd, ${misc:Depends}, ${shlibs:Depends}, Recommends: e2fsprogs, - linux-image-amd64, + linux-image-amd64 [amd64], + linux-image-arm64 [arm64], systemd-resolved, Suggests: libslirp-helper, user-mode-linux Description: Create and spawn virtual machines (program) From b7e1ed691fd569fc42a82971032c1e1f6b12456b Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Mon, 24 Jul 2023 16:23:20 +0100 Subject: [PATCH 38/52] Update changelog for 0.0.5-1 release --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 3a0e11e..1976407 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +golang-github-go-debos-fakemachine (0.0.5-1) unstable; urgency=medium + + * Bump Standards-Version to 4.6.2 + * New upstream version 0.0.5 + * Build fakemachine for arm64 + + -- Christopher Obbard Mon, 24 Jul 2023 16:23:08 +0100 + golang-github-go-debos-fakemachine (0.0.4-1) unstable; urgency=medium * New upstream version 0.0.4 From 9e69cb032cc96898f980af6a3809d09dacdc6427 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 27 Sep 2023 09:19:52 +0100 Subject: [PATCH 39/52] New upstream version 0.0.6 --- .github/workflows/ci.yml | 6 +++--- backend_qemu.go | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1c4c2f..e319907 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v4 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -24,7 +24,7 @@ jobs: name: Check if man page has been regenerated runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: run: | sudo apt-get update @@ -68,7 +68,7 @@ jobs: TMP: /scratch steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test build run: go build -o fakemachine cmd/fakemachine/main.go diff --git a/backend_qemu.go b/backend_qemu.go index 840e7bd..c5ab53d 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -211,6 +211,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { "-kernel", kernelPath, "-initrd", m.initrdpath, "-display", "none", + "-nic", "user,model=virtio-net-pci", "-no-reboot"} if kvm { @@ -235,6 +236,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { qemuargs = append(qemuargs, "-chardev", "stdio,id=for-ttyS0,signal=off", "-serial", "chardev:for-ttyS0") + kernelargs = append(kernelargs, "loglevel=7") } else { qemuargs = append(qemuargs, // Create the bus for virtio consoles From 5d60c8f40d055ec78c23bb25666499dcc67b24e5 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 27 Sep 2023 09:20:06 +0100 Subject: [PATCH 40/52] Update changelog for 0.0.6-1 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 1976407..364c92f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +golang-github-go-debos-fakemachine (0.0.6-1) unstable; urgency=medium + + * New upstream version 0.0.6 + + -- Christopher Obbard Wed, 27 Sep 2023 09:19:56 +0100 + golang-github-go-debos-fakemachine (0.0.5-1) unstable; urgency=medium * Bump Standards-Version to 4.6.2 From 030d03478b8cc3cccdbb07a054e3033a836ae12f Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 12 Nov 2023 13:12:37 +0000 Subject: [PATCH 41/52] New upstream version 0.0.7 --- .golangci.yml | 1 + backend.go | 10 +++++++++- backend_qemu.go | 1 + backend_uml.go | 2 +- cmd/fakemachine/main.go | 8 +++++--- go.mod | 1 + go.sum | 2 ++ machine.go | 12 ++++-------- machine_test.go | 35 ++++++++++++++++++++++++++++------- 9 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6b63b2b..9d44f1a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,6 @@ linters: enable: + - errorlint - gofmt - stylecheck - whitespace diff --git a/backend.go b/backend.go index 478911a..9ca2a7f 100644 --- a/backend.go +++ b/backend.go @@ -51,7 +51,15 @@ func newBackend(name string, m *Machine) (backend, error) { b, backendErr := newBackend(backendName, m) if backendErr != nil { - err = fmt.Errorf("%v, %v", err, backendErr) + /* Append the error to any existing backend creation error(s). + * Since we cannot join errors together in golang <1.20, instead + * join the error messages strings and return that as a new error. + */ + if err != nil { + err = fmt.Errorf("%v, %v", err.Error(), backendErr.Error()) + } else { + err = backendErr + } continue } return b, nil diff --git a/backend_qemu.go b/backend_qemu.go index c5ab53d..21c70bd 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -225,6 +225,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { qemuargs = append(qemuargs, "-machine", qemuMachine.machine) console := fmt.Sprintf("console=%s", qemuMachine.console) kernelargs := []string{console, "panic=-1", + "plymouth.enable=0", "systemd.unit=fakemachine.service"} if m.showBoot { diff --git a/backend_uml.go b/backend_uml.go index fa5a589..bbdd096 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -205,7 +205,7 @@ func (b umlBackend) Start() (bool, error) { "mem=" + memory + "M", "initrd=" + m.initrdpath, "panic=-1", - "nosplash", + "plymouth.enable=0", "systemd.unit=fakemachine.service", "console=tty0", } diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index 6e69926..91d32eb 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -1,7 +1,9 @@ package main import ( + "errors" "fmt" + "github.com/alessio/shellescape" "github.com/docker/go-units" "github.com/go-debos/fakemachine" "github.com/jessevdk/go-flags" @@ -141,8 +143,8 @@ func main() { args, err := parser.Parse() if err != nil { - flagsErr, ok := err.(*flags.Error) - if ok && flagsErr.Type == flags.ErrHelp { + var flagsErr *flags.Error + if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp { os.Exit(0) } else { os.Exit(1) @@ -180,7 +182,7 @@ func main() { command := "/bin/bash" if len(args) > 0 { - command = strings.Join(args, " ") + command = shellescape.QuoteCommand(args) } ret, err := m.Run(command) diff --git a/go.mod b/go.mod index ece4cb5..f779e0c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/go-debos/fakemachine go 1.15 require ( + github.com/alessio/shellescape v1.4.2 github.com/docker/go-units v0.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/klauspost/compress v1.15.3 diff --git a/go.sum b/go.sum index f921beb..a0183b1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/machine.go b/machine.go index 1fdfc35..c3e5817 100644 --- a/machine.go +++ b/machine.go @@ -7,6 +7,7 @@ import ( "bytes" "errors" "fmt" + "github.com/alessio/shellescape" "io/ioutil" "os" "os/exec" @@ -894,8 +895,8 @@ func (m *Machine) Run(command string) (int, error) { func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { name := path.Join("/", path.Base(os.Args[0])) - // FIXME: shell escaping? - command := strings.Join(append([]string{name}, args...), " ") + quotedArgs := shellescape.QuoteCommand(args) + command := strings.Join([]string{name, quotedArgs}, " ") executable, err := exec.LookPath(os.Args[0]) @@ -909,10 +910,5 @@ func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { // RunInMachine runs the caller binary inside the fakemachine with the same // commandline arguments as the parent func (m *Machine) RunInMachine() (int, error) { - name := path.Join("/", path.Base(os.Args[0])) - - // FIXME: shell escaping? - command := strings.Join(append([]string{name}, os.Args[1:]...), " ") - - return m.startup(command, [][2]string{{os.Args[0], name}}) + return m.RunInMachineWithArgs(os.Args[1:]) } diff --git a/machine_test.go b/machine_test.go index bcdc6cd..3d2ed86 100644 --- a/machine_test.go +++ b/machine_test.go @@ -12,9 +12,11 @@ import ( ) var backendName string +var testArg string func init() { flag.StringVar(&backendName, "backend", "auto", "Fakemachine backend to use") + flag.StringVar(&testArg, "testarg", "", "Test specific argument") } func CreateMachine(t *testing.T) *Machine { @@ -90,7 +92,7 @@ func TestScratchTmp(t *testing.T) { m := CreateMachine(t) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchTmp"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchTmp"}) if exitcode != 0 { t.Fatalf("Test for tmpfs mount on scratch failed with %d", exitcode) @@ -107,7 +109,7 @@ func TestScratchDisk(t *testing.T) { m := CreateMachine(t) m.SetScratch(1024*1024*1024, "") - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchDisk"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchDisk"}) if exitcode != 0 { t.Fatalf("Test for device mount on scratch failed with %d", exitcode) @@ -145,7 +147,7 @@ func TestSpawnMachine(t *testing.T) { m := CreateMachine(t) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestSpawnMachine"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestSpawnMachine"}) if exitcode != 0 { t.Fatalf("Test for respawning in the machine failed failed with %d", exitcode) @@ -180,7 +182,7 @@ func TestImageLabel(t *testing.T) { labeled, err := m.CreateImageWithLabel("test-labeled.img", 1024*1024, "test-labeled") require.Nil(t, err) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestImageLabel", autolabel, labeled}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestImageLabel", autolabel, labeled}) if exitcode != 0 { t.Fatalf("Test for images in the machine failed failed with %d", exitcode) } @@ -197,7 +199,7 @@ func TestVolumes(t *testing.T) { m := CreateMachine(t) m.AddVolume("random_directory_never_exists") - exitcode, err := m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err := m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) @@ -205,7 +207,7 @@ func TestVolumes(t *testing.T) { m = CreateMachine(t) m.AddVolume("/dev/zero") - exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) @@ -213,7 +215,26 @@ func TestVolumes(t *testing.T) { m = CreateMachine(t) m.AddVolumeAt("/dev", "/dev ices") - exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) } + +func TestCommandEscaping(t *testing.T) { + t.Parallel() + if InMachine() { + t.Log("Running in the machine") + require.Equal(t, testArg, "$s'n\\akes") + t.Log(testArg) + return + } + + m := CreateMachine(t) + exitcode, _ := m.RunInMachineWithArgs([]string{ + "-test.v", "-test.run", + "TestCommandEscaping", "-testarg", "$s'n\\akes"}) + + if exitcode != 0 { + t.Fatalf("Expected 0 but got %d", exitcode) + } +} From 05b59cd716ed1421c38983e73593fcca1aed466e Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 12 Nov 2023 13:12:37 +0000 Subject: [PATCH 42/52] New upstream version 0.0.7 --- .golangci.yml | 1 + backend.go | 10 +++++++++- backend_qemu.go | 1 + backend_uml.go | 2 +- cmd/fakemachine/main.go | 8 +++++--- go.mod | 1 + go.sum | 2 ++ machine.go | 12 ++++-------- machine_test.go | 35 ++++++++++++++++++++++++++++------- 9 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6b63b2b..9d44f1a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,6 @@ linters: enable: + - errorlint - gofmt - stylecheck - whitespace diff --git a/backend.go b/backend.go index 478911a..9ca2a7f 100644 --- a/backend.go +++ b/backend.go @@ -51,7 +51,15 @@ func newBackend(name string, m *Machine) (backend, error) { b, backendErr := newBackend(backendName, m) if backendErr != nil { - err = fmt.Errorf("%v, %v", err, backendErr) + /* Append the error to any existing backend creation error(s). + * Since we cannot join errors together in golang <1.20, instead + * join the error messages strings and return that as a new error. + */ + if err != nil { + err = fmt.Errorf("%v, %v", err.Error(), backendErr.Error()) + } else { + err = backendErr + } continue } return b, nil diff --git a/backend_qemu.go b/backend_qemu.go index c5ab53d..21c70bd 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -225,6 +225,7 @@ func (b qemuBackend) StartQemu(kvm bool) (bool, error) { qemuargs = append(qemuargs, "-machine", qemuMachine.machine) console := fmt.Sprintf("console=%s", qemuMachine.console) kernelargs := []string{console, "panic=-1", + "plymouth.enable=0", "systemd.unit=fakemachine.service"} if m.showBoot { diff --git a/backend_uml.go b/backend_uml.go index fa5a589..bbdd096 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -205,7 +205,7 @@ func (b umlBackend) Start() (bool, error) { "mem=" + memory + "M", "initrd=" + m.initrdpath, "panic=-1", - "nosplash", + "plymouth.enable=0", "systemd.unit=fakemachine.service", "console=tty0", } diff --git a/cmd/fakemachine/main.go b/cmd/fakemachine/main.go index 6e69926..91d32eb 100644 --- a/cmd/fakemachine/main.go +++ b/cmd/fakemachine/main.go @@ -1,7 +1,9 @@ package main import ( + "errors" "fmt" + "github.com/alessio/shellescape" "github.com/docker/go-units" "github.com/go-debos/fakemachine" "github.com/jessevdk/go-flags" @@ -141,8 +143,8 @@ func main() { args, err := parser.Parse() if err != nil { - flagsErr, ok := err.(*flags.Error) - if ok && flagsErr.Type == flags.ErrHelp { + var flagsErr *flags.Error + if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp { os.Exit(0) } else { os.Exit(1) @@ -180,7 +182,7 @@ func main() { command := "/bin/bash" if len(args) > 0 { - command = strings.Join(args, " ") + command = shellescape.QuoteCommand(args) } ret, err := m.Run(command) diff --git a/go.mod b/go.mod index ece4cb5..f779e0c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/go-debos/fakemachine go 1.15 require ( + github.com/alessio/shellescape v1.4.2 github.com/docker/go-units v0.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/klauspost/compress v1.15.3 diff --git a/go.sum b/go.sum index f921beb..a0183b1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/machine.go b/machine.go index 1fdfc35..c3e5817 100644 --- a/machine.go +++ b/machine.go @@ -7,6 +7,7 @@ import ( "bytes" "errors" "fmt" + "github.com/alessio/shellescape" "io/ioutil" "os" "os/exec" @@ -894,8 +895,8 @@ func (m *Machine) Run(command string) (int, error) { func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { name := path.Join("/", path.Base(os.Args[0])) - // FIXME: shell escaping? - command := strings.Join(append([]string{name}, args...), " ") + quotedArgs := shellescape.QuoteCommand(args) + command := strings.Join([]string{name, quotedArgs}, " ") executable, err := exec.LookPath(os.Args[0]) @@ -909,10 +910,5 @@ func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { // RunInMachine runs the caller binary inside the fakemachine with the same // commandline arguments as the parent func (m *Machine) RunInMachine() (int, error) { - name := path.Join("/", path.Base(os.Args[0])) - - // FIXME: shell escaping? - command := strings.Join(append([]string{name}, os.Args[1:]...), " ") - - return m.startup(command, [][2]string{{os.Args[0], name}}) + return m.RunInMachineWithArgs(os.Args[1:]) } diff --git a/machine_test.go b/machine_test.go index bcdc6cd..3d2ed86 100644 --- a/machine_test.go +++ b/machine_test.go @@ -12,9 +12,11 @@ import ( ) var backendName string +var testArg string func init() { flag.StringVar(&backendName, "backend", "auto", "Fakemachine backend to use") + flag.StringVar(&testArg, "testarg", "", "Test specific argument") } func CreateMachine(t *testing.T) *Machine { @@ -90,7 +92,7 @@ func TestScratchTmp(t *testing.T) { m := CreateMachine(t) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchTmp"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchTmp"}) if exitcode != 0 { t.Fatalf("Test for tmpfs mount on scratch failed with %d", exitcode) @@ -107,7 +109,7 @@ func TestScratchDisk(t *testing.T) { m := CreateMachine(t) m.SetScratch(1024*1024*1024, "") - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestScratchDisk"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchDisk"}) if exitcode != 0 { t.Fatalf("Test for device mount on scratch failed with %d", exitcode) @@ -145,7 +147,7 @@ func TestSpawnMachine(t *testing.T) { m := CreateMachine(t) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestSpawnMachine"}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestSpawnMachine"}) if exitcode != 0 { t.Fatalf("Test for respawning in the machine failed failed with %d", exitcode) @@ -180,7 +182,7 @@ func TestImageLabel(t *testing.T) { labeled, err := m.CreateImageWithLabel("test-labeled.img", 1024*1024, "test-labeled") require.Nil(t, err) - exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run TestImageLabel", autolabel, labeled}) + exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestImageLabel", autolabel, labeled}) if exitcode != 0 { t.Fatalf("Test for images in the machine failed failed with %d", exitcode) } @@ -197,7 +199,7 @@ func TestVolumes(t *testing.T) { m := CreateMachine(t) m.AddVolume("random_directory_never_exists") - exitcode, err := m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err := m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) @@ -205,7 +207,7 @@ func TestVolumes(t *testing.T) { m = CreateMachine(t) m.AddVolume("/dev/zero") - exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) @@ -213,7 +215,26 @@ func TestVolumes(t *testing.T) { m = CreateMachine(t) m.AddVolumeAt("/dev", "/dev ices") - exitcode, err = m.RunInMachineWithArgs([]string{"-test.run TestVolumes"}) + exitcode, err = m.RunInMachineWithArgs([]string{"-test.run", "TestVolumes"}) require.Equal(t, exitcode, -1) require.Error(t, err) } + +func TestCommandEscaping(t *testing.T) { + t.Parallel() + if InMachine() { + t.Log("Running in the machine") + require.Equal(t, testArg, "$s'n\\akes") + t.Log(testArg) + return + } + + m := CreateMachine(t) + exitcode, _ := m.RunInMachineWithArgs([]string{ + "-test.v", "-test.run", + "TestCommandEscaping", "-testarg", "$s'n\\akes"}) + + if exitcode != 0 { + t.Fatalf("Expected 0 but got %d", exitcode) + } +} From 5c0bcb1379adc8c6111bf19b01f273dcf6eff3ce Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 12 Nov 2023 13:13:20 +0000 Subject: [PATCH 43/52] Update obbardc's email address in Maintainers Signed-off-by: Christopher Obbard --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index d5e1fe6..4e21235 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Debian Go Packaging Team Uploaders: Andrej Shadura , - Christopher Obbard , + Christopher Obbard , Héctor Orón Martínez , Build-Depends: debhelper-compat (= 13), From 2849c71975735403515f34c64845120e4be56bc8 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 12 Nov 2023 13:15:57 +0000 Subject: [PATCH 44/52] Add golang-github-alessio-shellescape-dev to Build-Depends Signed-off-by: Christopher Obbard --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 4e21235..645c5ce 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: debhelper-compat (= 13), dh-golang, golang-any, + golang-github-alessio-shellescape-dev, golang-github-docker-go-units-dev, golang-github-klauspost-compress-dev, golang-github-stretchr-testify-dev, From a3955d021efd571c276852e3c7836ac9d0119464 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 12 Nov 2023 13:19:25 +0000 Subject: [PATCH 45/52] Update changelog for 0.0.7-1 release Signed-off-by: Christopher Obbard --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 364c92f..a4e4ea1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +golang-github-go-debos-fakemachine (0.0.7-1) unstable; urgency=medium + + * New upstream version 0.0.7 + * Update obbardc's email address in Maintainers + * Add golang-github-alessio-shellescape-dev to Build-Depends + + -- Christopher Obbard Sun, 12 Nov 2023 13:18:56 +0000 + golang-github-go-debos-fakemachine (0.0.6-1) unstable; urgency=medium * New upstream version 0.0.6 From 65befdcde4c2c054642dcda1855ddd08eda369ba Mon Sep 17 00:00:00 2001 From: Arnaud Rebillout Date: Thu, 23 Nov 2023 11:37:42 +0700 Subject: [PATCH 46/52] Add golang-github-alessio-shellescape-dev to -dev Depends This new build dep was added in 2849c71975735403515f34c64845120e4be56bc8 and I believe it should have been added to the Depends for the library package, otherwise consumers of golang-github-go-debos-fakemachine-dev fail to build, as seen in https://bugs.debian.org/1056140. --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 645c5ce..3aa6318 100644 --- a/debian/control +++ b/debian/control @@ -50,6 +50,7 @@ Package: golang-github-go-debos-fakemachine-dev Architecture: amd64 Built-Using: ${misc:Built-Using} Depends: + golang-github-alessio-shellescape-dev, golang-github-docker-go-units-dev, golang-github-klauspost-compress-dev, golang-github-stretchr-testify-dev, From 1905fbe190df9f1b94c25aab72639ef182737c0a Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 24 Nov 2023 13:34:05 +0000 Subject: [PATCH 47/52] Release 0.0.7-2 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index a4e4ea1..9f70e30 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +golang-github-go-debos-fakemachine (0.0.7-2) unstable; urgency=medium + + [ Arnaud Rebillout ] + * Add golang-github-alessio-shellescape-dev to -dev Depends + + -- Christopher Obbard Fri, 24 Nov 2023 13:33:54 +0000 + golang-github-go-debos-fakemachine (0.0.7-1) unstable; urgency=medium * New upstream version 0.0.7 From 3296cfa073722383c273530a819df15718147793 Mon Sep 17 00:00:00 2001 From: Arnaud Ferraris Date: Fri, 1 Dec 2023 14:08:47 +0100 Subject: [PATCH 48/52] d/control: also build -dev package for arm64 This will allow building `debos` for this architecture as well. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 3aa6318..0c5e3fc 100644 --- a/debian/control +++ b/debian/control @@ -47,7 +47,7 @@ Description: Create and spawn virtual machines (program) This package contains the fakemachine program. Package: golang-github-go-debos-fakemachine-dev -Architecture: amd64 +Architecture: amd64 arm64 Built-Using: ${misc:Built-Using} Depends: golang-github-alessio-shellescape-dev, From f76ea6cc6299bfa79e6f7b43733e0808c7e0e4f2 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 10 Jan 2024 16:49:31 +0000 Subject: [PATCH 49/52] New upstream version 0.0.8 --- .github/workflows/ci.yml | 15 +++++++++------ backend.go | 3 --- backend_qemu.go | 4 ---- backend_uml.go | 4 ---- go.mod | 4 ++-- go.sum | 8 ++++---- machine.go | 22 +++++++++++++++++++--- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e319907..8dd1974 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -48,22 +48,25 @@ jobs: # See https://github.com/actions/runner-images/issues/183 # # For Arch Linux uml is not yet supported, so only test under qemu there. - os: [bullseye, bookworm, trixie] - backend: [qemu, uml] + os: [bookworm, trixie] + backend: [qemu, uml, kvm] include: - os: arch - backend: qemu + backend: "qemu" + - os: arch + backend: "kvm" name: Test ${{matrix.os}} with ${{matrix.backend}} backend - runs-on: ubuntu-latest + runs-on: ${{ matrix.backend == 'kvm' && 'kvm' || 'ubuntu-latest' }} defaults: run: shell: bash container: - image: ghcr.io/go-debos/test-containers/fakemachine-${{matrix.os}}:main + image: ghcr.io/go-debos/test-containers/${{matrix.os}}:main options: >- --security-opt label=disable --cap-add=SYS_PTRACE --tmpfs /scratch:exec + ${{ matrix.backend == 'kvm' && '--device /dev/kvm' || '' }} env: TMP: /scratch steps: diff --git a/backend.go b/backend.go index 9ca2a7f..b5d8b1a 100644 --- a/backend.go +++ b/backend.go @@ -105,9 +105,6 @@ type backend interface { // A list of udev rules UdevRules() []string - // The match expression used for the networkd configuration - NetworkdMatch() string - // The tty used for the job output JobOutputTTY() string diff --git a/backend_qemu.go b/backend_qemu.go index 21c70bd..191ad79 100644 --- a/backend_qemu.go +++ b/backend_qemu.go @@ -162,10 +162,6 @@ func (b qemuBackend) UdevRules() []string { return udevRules } -func (b qemuBackend) NetworkdMatch() string { - return "e*" -} - func (b qemuBackend) JobOutputTTY() string { // By default we send job output to the second virtio console, // reserving /dev/ttyS0 for boot messages (which we ignore) diff --git a/backend_uml.go b/backend_uml.go index bbdd096..7bf6eac 100644 --- a/backend_uml.go +++ b/backend_uml.go @@ -100,10 +100,6 @@ func (b umlBackend) UdevRules() []string { return udevRules } -func (b umlBackend) NetworkdMatch() string { - return "vec*" -} - func (b umlBackend) JobOutputTTY() string { // Send the fakemachine job output to the right console if b.machine.showBoot { diff --git a/go.mod b/go.mod index f779e0c..f98c990 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/alessio/shellescape v1.4.2 github.com/docker/go-units v0.5.0 github.com/jessevdk/go-flags v1.5.0 - github.com/klauspost/compress v1.15.3 + github.com/klauspost/compress v1.17.4 github.com/stretchr/testify v1.8.1 github.com/surma/gocpio v1.1.0 github.com/ulikunitz/xz v0.5.11 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + golang.org/x/sys v0.15.0 ) diff --git a/go.sum b/go.sum index a0183b1..551be47 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/klauspost/compress v1.15.3 h1:wmfu2iqj9q22SyMINp1uQ8C2/V4M1phJdmH9fG4nba0= -github.com/klauspost/compress v1.15.3/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -23,8 +23,8 @@ github.com/surma/gocpio v1.1.0/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/machine.go b/machine.go index c3e5817..36a2f4e 100644 --- a/machine.go +++ b/machine.go @@ -286,7 +286,7 @@ exec /lib/systemd/systemd ` const networkdTemplate = ` [Match] -Name=%[1]s +Type=ether [Network] DHCP=ipv4 @@ -294,8 +294,18 @@ DHCP=ipv4 LinkLocalAddressing=no IPv6AcceptRA=no ` + +const networkdLinkTemplate = ` +[Match] +Type=ether + +[Link] +# Give the interface a static name +Name=ethernet0 +` + const commandWrapper = `#!/bin/sh -/lib/systemd/systemd-networkd-wait-online -q +/lib/systemd/systemd-networkd-wait-online -q --interface=ethernet0 if [ $? != 0 ]; then echo "WARNING: Network setup failed" echo "== Journal ==" @@ -798,7 +808,13 @@ func (m *Machine) startup(command string, extracontent [][2]string) (int, error) } err = w.WriteFile("/etc/systemd/network/ethernet.network", - fmt.Sprintf(networkdTemplate, backend.NetworkdMatch()), 0444) + networkdTemplate, 0444) + if err != nil { + return -1, err + } + + err = w.WriteFile("/etc/systemd/network/10-ethernet.link", + networkdLinkTemplate, 0444) if err != nil { return -1, err } From cdbd71628a320f3cf770aa50140f55b8c1c3c929 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 10 Jan 2024 16:50:58 +0000 Subject: [PATCH 50/52] Release 0.0.8-1 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9f70e30..798c505 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +golang-github-go-debos-fakemachine (0.0.8-1) unstable; urgency=medium + + [ Arnaud Ferraris ] + * d/control: also build -dev package for arm64 + + [ Christopher Obbard ] + * New upstream version 0.0.8 + + -- Christopher Obbard Wed, 10 Jan 2024 16:50:45 +0000 + golang-github-go-debos-fakemachine (0.0.7-2) unstable; urgency=medium [ Arnaud Rebillout ] From 39dad95063aa7ccf627e2991254fef6797d2ffde Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 17 Jan 2024 15:17:07 +0000 Subject: [PATCH 51/52] New upstream version 0.0.9 --- go.mod | 2 +- go.sum | 4 ++-- machine.go | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f98c990..29415d4 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( github.com/stretchr/testify v1.8.1 github.com/surma/gocpio v1.1.0 github.com/ulikunitz/xz v0.5.11 - golang.org/x/sys v0.15.0 + golang.org/x/sys v0.16.0 ) diff --git a/go.sum b/go.sum index 551be47..b66c8f0 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/surma/gocpio v1.1.0/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/machine.go b/machine.go index 36a2f4e..d07208b 100644 --- a/machine.go +++ b/machine.go @@ -238,6 +238,15 @@ func NewMachineWithBackend(backendName string) (*Machine, error) { m.AddVolume("/etc/ssl") } + // Mounts for java VM configuration, especialy security policies + matches, _ := filepath.Glob("/etc/java*") + for _, path := range matches { + stat, err := os.Stat(path) + if err == nil && stat.IsDir() { + m.AddVolume(path) + } + } + // Dbus configuration if _, err := os.Stat("/etc/dbus-1"); err == nil { m.AddVolume("/etc/dbus-1") From c0a2d167e3728b82d60c1f7ec9358f8ce61900e6 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Wed, 17 Jan 2024 15:17:45 +0000 Subject: [PATCH 52/52] Release 0.0.9-1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 798c505..d722ea1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +golang-github-go-debos-fakemachine (0.0.9-1) unstable; urgency=medium + + * New upstream version 0.0.9 + + -- Christopher Obbard Wed, 17 Jan 2024 15:17:12 +0000 + golang-github-go-debos-fakemachine (0.0.8-1) unstable; urgency=medium [ Arnaud Ferraris ]