From a7949c548e6aa4b6337c9783fd448e7d5a270cab Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 21 Oct 2022 12:21:23 +0100 Subject: [PATCH] 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 zcmV+pK=HpQwJ-f-06zf$0t$)cAlfXa^l{i9cy+`=5t zBA{$gKZmG>tNxinOY8w_E=G1JtMJlxSP6x>7@XDuJ$)z4EMJ{Ix*RfH@PpQ67)jrU zLsPBHd)prnfze6)o$Wj0wp@P00xqOC@_oL2S?7uzSMv;f9bYC2X_fg|L=XrZZ$-v{ z@0-2?vBUy(Z9MM4QvJJHtNaNB$@zt1H`FsUu?w9oNdMGYTYjnrwsxy-;1{D^@A*hDi>usNpHf-np5hXo)>ZCMwGlZO(3(0|e(u)|IB^#g>*s!X-2 zxj=LS3D9tWATCK+FS~Dy>aRlo!>Yx8hTYczID0oU}Cb0&hji9+}hWro{ zpNumd#E-+|JfUH|x;~QfCq1u?ZkQz`HYH`QdXfSVT?0$15RtIuO%)%HxP0VtsP3M>6sr zu@}GYiLSax*}!~T5l0L5(&JJ!>-Vs&krks17rxcLI*lD>00{R1`|uZzsM@WU_%Qhs z1l=)Og3Pm;5JasHa9A}3D-ym=n!SN;{LpyODWgq(et!vHTL|SFB-%?B3@S`~5mg62#tdXHQ z9d%91GsZ=0q)vhyul>9%`LWJ+^$o0kDhnQR=pCqBHrI2d%4|OBwuoBP5-(weQTa18 zDU6Wx8E@FDQ2Fsvhm3g%*{0%(aW9v%=&!{{*9M-pc4>n#=NBMerODYrDNXcbdyE|e zHZ?R50q?A{<+q5U_B>WvEv5ByBz7T${IU4%?JUtdxsch(&lGOCq(L*86->Rm@w{Rs zR24yX8r&DrmoUk|;H5s!A7UvqzCI;kw{2<+v$o+ekqL}^1ah9$;?wh;daShm#bbG2 zm476Az?K43Lc!~Ace#iS$zFSJP@VDAY8}GE1~2qrEE1VNE~$Pbn0{l91c>CFhzEFm zeD3_#(4c0DxHJ(dl~=Hd^&f&f)AQfC(enXyDV_#9LcCTjU)5FE%7~V^wIsW-T;|OB z25qJ;$luDRTNkBJP)pPF4TEr~?qPu-ZxT;1+Dj}+=`+c#>ft%EfM5=169Xa$a>^P` zm6l|Jr4<%*X|WVliKZwW>6C*I%#>1PtN0@EfMh~af+1i+x@@%!SvvsU&0sLD(7k@kjz%|R7{&I)2{ueve2LhKP zNY6lA_40?A=ni*geAZX|ZOfU!+oD; zqUE%1*HggMMG{*(22rSnNzG9%;7V0D z7>apvlSaIY4M?Lp_rke;JG{;e`6w6mGD3{s#i};Hbx>|PKfZzfG z0L*}=YqMR1#oj9HtswIq`b_aU#VPIyfOW}F9nvmkwt>Y*#XEqq($-R)fjg+)kKeU< zp21EO-ap=%zwh#v94MvgJMN-C_kn*=U8=|Z+VjrG?b93ZP*pSN^ep(_X zrUB5LJP)#{&v13)#2>LNSnMf*I)Rg`{=HzWvryq2Y!vfpLO%+S7z*zC-2Jb0W}hNZ zB)H#j=O+(Z`F*K~;lOP47@CnrnNM0=+N0^WN4(TgzcQe8>>;D&TZcc-BTM6l-Iw{Q zyA+k3)bpa@LSW@8?wq~qHa46a>w2w6%jHQSvq#Ery|yqrAP{<=wM1<26}Ia1CTbg5 zCcD26){i>-qLPnw5eNQoiXEL62Uu)3r!gPM)O>yVI?(8WGN-9Aj*BvQYF+uOlO?e%L;-0F&`Wv(YETo4ztQfrfuZQgAIjv5O=q zk+AG~*0B#qZj2?#|{n1!U9D>YOY>A(u{bc}(8hAm?0{!l*_(73?@fIdm$8XRP^X@MnpxNr>MDL!UN>(71ZN*o(_j6)CwXlRc%yMwvm=S&J{Na1N_s;<_L3to`nf2<}!N4s#@qq~~d-=OiWE@F@ml?X(cSf+&H*RNi zEHya`@QJE`SA%V$Oy;~m;8X|P$dcSEEY!)*D|1(~rpE?;@R;r>4#;aWsK{yD)yPOi z^4e}P=J%JX;yu6FB0k5%*7awFNDry+^b$#bS)E=4e?r*@?$mf`u5W~1#;jbV z4oBfW>5H<5?5i7Y@g~49?DmYq&**f_|EihO&ITBHs-V4q6)Q-5VRpF#d6>;v3OnV9 zY;&}<8j`$+@sJNA9@lLO(ShDC0yI>Z*7((CEpHd!Ou|$m1-_QMGJA@~`CL{XaK<>F zTvYeuBwBGvB=3Nf^-JbbIUD*1?s$Z@wzN=Z!pVHh!9Gh!lI4cB@FpP6rAGg2We@m6 z?0D0F&ZyhfHpd)A9tBIV^LO+@#~6np!c(B}gq55*Qfl0pqbJEO5%e6yOH+jDmrX;} zKhn42Z55a^Tf0acteg{iuvFF3NbwI-0XkzD%DWl&QPgp^ z9+36mRjnhiWMrE#(az#rGg4$Ro%l~Okkf?RM@^ZgQ~H3LRQlNQzqCXV#;2L8HIGYv zRgJt-C@#>lNPb4$)3KA*eY_fEtRxa!Gg?Ii>?9#OqK7~19c==j>}v(@nU?w*mIytW znbiY#k9*#|iMhRwu|DE_ofJGz-((WH+N;i&RV(=Qf;}40Xk5P{{{Ize)?wzrNB7{( ztBlG1`4#B<(&V8PV8;TVLdh4Y0_G#!89_7zujbGe&~bdyZMpYNqqgyZ4!Y5Nbfst8 zBE3ozz*Vb?`{CG%L5<1sKom@B-p)_*d4_!@)VSC5CeqSKWOxGvh7jU@Az-Wr!HMc{ zitV~M1qgjsFL|?_EiT|eTHqJ8Hixygp1iC#XPAo;PD19w^N=Aph4z&6mOZM>0eN%l zRT7YOoJKixDpiY(P-Yip7{G;FJaKN~>IQ82q@>zWoc7_dU$MKPf^ity2>B})W~q7l z<4&Q{Zf?g7Yr$OoyMC_Ed8M>p-ck~aByWOBmcE+){P)j8dY%rV%PK8fkP*QXraD%u zO_F_6Y^J>jG9b6d=fP)^V8n=#~rlyo5LaH%iDvfKNc9`h!18L?Rl z$}3gisW3&N2I%*nLJiBrj2~vUHD2zB1?%obo_sa1v#0wa;% zyWSL8wK!2GkkhJzg@OBnAFEv;9s9IC?^fc|(c!`N*5o>!SD2?{mn?Oe;~gGAU5r6b z)W~Y9kw7U%3H#E3U|DLr`kBvvCc>ufvAhR>bikHMq#cV5PgX%PgkWI(dXT8+o^Z$x zr^QlQNWoxFy}(BPe2KKFPcx+iDL4l?`mj%ixU#HTJj7!!c&ce*Lw%{(V+Y13oAE3j zg6!^ZOqsz6FV!)kJ)I{-ZzZpaZrkZ}o|oC3#u_*97^qP;0Bk`FjsHZ#F2aHfj7{p@^uIH)=b<{JjSnHTv7L~e^&cEEKaJtd+fzTJ+l6d7?uYTQz}2g1UAA(9#p z{E*rDBnyPq37V}hqA5vMZez^&DIL;o{HHb+&Ep~3GkN|hsk2X((v)$! zxnr6?i*s{$j1a3;Z(pbE(}MHZpty0Xfc!+eXji)m0Ja?vT)ZrkElMl11OYe6t5ia= zNO*YvrvEuiw*8qT&bzXd2NL-#@WR zmtNIZC6kjmW#!IR!K8}jTv0nhxX)-WwDICnOiRGAr`FOg;)dP@w@o>10@~*VF;hd9 zRo7O_Gk_C|ST!MBiYdGOjz^h1s48LD~ke2n757^Gl#U}0)T(I6lZwOGvU$meX({+2BM%$k^8c2nYc zoToQw?ZV3{cL??u|5=;RgYC7yn|?{gPevJz;_W(l7aR0H+M!#mdKOhame;pZHi81H zaUle|Qhm2Mz{_zGD{;jwf8LYZh<8dLpui=`PCWL6?GehCn6YH9=r#yAv@i>ul&Gt8 zI_5=}pyN9n6N zK>s(3!v|ou`cV-$p-*(%O>n~nwI{y7@U-?+h;uuIcl?f~=wMX$&fQlm>%rB3| zw?nhYBhYW1y~b!#BU**nO_v^cO=+(Kl1ZjYDlWAN@M#&G6v-f)qn4J+Drm?(I~&lB zP92vMqrX5yw+w!0f-n?tvGH@5HY8^AR zf2Wb+g&#wg?QaRP#+<9-r)LL;kU9lheaN%V@B1Zqju>f-vQ36`YVX z^$L(EAtZ>&=KJE0=x6o&0y5ErM;1UVT)Qqz>Xsj+M|we~{dT<;&Zo*0%#p6EYej&n ztBL_JrY_nnCxp%$N!XMrzajYq6n2peZROP_ z3I!bmBG>iG9HhIDK?jq-gd4Zv&47GEKC~a3-AgM{MCow zjLw3H2iHM5@pL$;F-QJ5B!RsA;hViG)I!RFqV7_I#4poK3v(!r8F89sOY5WqI|VZ; zBRNO!glKCDKs{nF3F>&(+UCiq9-_>+8@sz^Jj_virC6E~$-L3^&T@!-HM7Se?ULea zvzMsbk0FTYCquzF#A!2vLuSF67+*i*)JEY&)1Dc&PmRRo5Z@QkJ@F`nY0`-U*O9t@ zn!oR9mx1rO>ai_J_C0?v{@x{jz#d!`c-6yUYgz9j50}+j(M$YVEhKUV>5(AS;~mOW z`5cd;F%GOU2gji?3!66%V@P&YW(L5G0lP7mT;Vaa7VARsXN2CDzqiN&Lq(Ha4LWlIgD}Q?qV-v4*eJ~am0$&yfC~4Z+;fb7M39$v)zI$ z8Ju2L305<&FEzV-M$yV|ld>fY7n1n(i(@PFM}X?YHe{=v&w+eT%^o&pz~~cGl_eG` zMmzAaY;O65+zAzzhDITmIJtrMG6Iim%L9Rqv6*qV4lG_ctW8ZHz#_WP zG49>69tQm9QOhCToYUD(9qFu|@^Z*rpY6U-)M*{GAUi7ptH;WIS{BGnuzBM*o4c|f zehuQ*7O|jqP1>ps-s7nPtMs7mM_C*K52IM(u{E<{eq;qkKbC9UJ^Tqa(ITM5fI6W> zrjhtH2S*E#NsHyxEG_Y6GJ*r8>q5`Wc$aBBFyAZ?>1NUN{A*36PxVl;M}XjfV^Z0Y z(m~8LH+=v#YF9t(h)t?R*nZ~(EuWWgHqw(-DKTWp$tZ$Ckw0I9l#*4J(d?zW0k@o} z{oT25^ikQAWrj%#%c({1s0<)r=i+ltC|_okukPYHjMq8E(T{--!5N5UEeJj>Ffx

>C@vkYMDT_4;tJ#+(pX}5(szwLZ9Lo=U=>bd zZN)BL^&2S!|4@ykVx$>!WMejcGpHidz)(YHgUXR(pO|B8!jiM*%er|xEAaO*AWPoC zkk*l-2(Yj$F(;h}2~^a?+G#$XY`>UAJ#>KQJkR#|rjM0EV;evFWaAR`o+5NW`M%MX zOl;p`UMwGlh&qg}DGZD#urKX5M7bbE%X%D8PXP`DAEc2%Clez=2Kzbe0&$Wo43<4J z4)_hExRFjU!V%)aPk?c*QEXdj2w0Y)Z`h&J%EqOVSr2)@KsSa4=agD{4`EKbyi+}Z zB5j;8g105D(S)a2BtPD>u@hJkRoFf4_J%r`GL3eMS_le~w)Dd+} zVfU3eJ)|cgl*^I?zH|0^L>*+AR)~|bK&SPTuaX=;44Ysp$6pcN&{l&t`i$hj`*!EdDZVKx*hYq4Y*h&P}54ox8byO zKmv*aU;V#@PTjuhk$1VZL7;1(fXbycuuoUrQRH1@?sWKWcG~!sP)v~rPQCY}`OC|t z2@-U~WFJV;0O%c(0~V!#!HM}>Lm@U70|wT8Ezeqz8?uKYyq7)<$@p-xI`r+#xVZan zd@IwicKi6cM#V8&kO3zYG8<%aR9$^%@`L(Qf?^MLc>pfZVDTModh3|uwwFZ z&iW%{faNN*6WKX(_V|WOQ}bUP?Hd&r9!**Oh=V#BAjiYtNn(hen*W@5`-Sh|owQ4n8?ZtQ1_2lL=`=cnCp2wR7qup^?&cEv>BB1}Kjhm|$Wk-5 zcF!H^+C$#5K$LP84;jCCP zupf!E2jrkGBxOnl?9vB=Mni0C>d{^1gnNmrn})}jht9S@nV9l_s+lX<$KFJsziGc3 zY~~@eI#SBfVxdvb&QKu<#MTrl-0Jle zOCrHv;*4rLz6$iw(t@OeyEyGJ7%wXCEQa+M+aqE?BRl)H(x8Gm|Cq9TGLO7dhpVr6 zZ@+0k$^(~iH)nw~ch9XP&E->76m{cvAwudVaUL>!2@wes{Y>EGIVFniuaX<8>~xi0 zWHexWO;h^&S)tnncA70GvCW+s8QGSWwmELYn_Sf0Z>1y4pqIqUAQ6E%NP`U`h$H+9 zr?xJOP8v=-eCxICv0NYUZ;F*3j|59n#eZa9X@n*_ANv7~o z9`0s(=1b#^Oowp~@HgtUWf3*vR8SB8NRCEXBC|iO8s>m&a;Klcp(8AVir95lux9;q z1B+H4xzTV>5%_6}FLK8;hJkrZh`3E&R?hXXOVH{rF2t+Q`t0dl(7H735_y=AgF_}i z-K>v@h?<5%qawt@jD^$gO=50eiQ95s-e`0^sP~j|W7vg!e7jNAn8Fo!FN7U57iz}t z15Y>2tsfi8=>M87icwf({GuRk5N?gXxC^!Npx5uTF^e~7mWgvPOpUkL2=VoR@N}cJ z^A5)Eo4#%~0*P~ZAVjBInquw5J1HSs;{UEOF#YF=$)Rf+`SU#Pb&kjiVxoFuG+1o|g2~Ms-ON>!&R8lH1#73bNK8pRK!K)9oS*Fr`Eo z^%Py2j!m^A$#JKtsZPH%qEh7!oz*^{Ma^^zDs*H;PU7MKwF&{(R392Bt>$zX{m(8w zJLnm{T6k3;0ta!SwPZ~)yoQG|j(31T2=X8NScsKQrHc>tz0i9rbycG$r4)xh)iQ=e zN|2D0qx3xo{)jSlzZOpVhPK{FH5#Wz;AusEL@jSs2?~uDM{Ul!C^yb77vEtTs7i)T z7XpI+W^vW(57)6n4bbD#RFy(7L?*T(GC%;?>#)eI0~ITE-w_K@c0vhMwqn1Ua4PNd zD2{kC11$;G-P>yl(}7`Zp`)ssZnxJ}p?56;A&^NC2XYYcx|S+{BYzWjOqr$}{$ln= zT%o+*EeiGHv1fMt0y8r(_#bQyKvr?iSKe?$i0E$~95lsc0!i1;UCXS03=qyk+xkt( z>+)}w{;RX7NNik_Sc##{2o1YD95+6|l5bS@ zwSUBE?gcLxp`}LEb6_xE6;e$!``S0d?ufTb7(3)L=VDQzX-N9X{yFH6I(!MEw!2a^ zR(#1MeDqdOiXW-WtCo=;DA<(&ff(45419tvgU~s`I7l%u5Us2eux>_*3-pOwY|6w9 z#Bq6rl&T7$lhc9f-lm0Y8qiu@OBY~HcMt!>iE;Cg1m?j{Db))4YCc4UMT&izw&oxv zZdJ8MQbTm3>&TZ_=CdRgL~%at3-}IC0A?n<>i4jCJ$E!ykrdTr1BdgPQL*Ds$F+Be zOweN3GI1&81Iwqeyz>cS1B0iE(i6CLG02beG9TV@9X*mn`FlTW2usIhvniBrtSG_8 z2TAgxY8nIxO)0s2+Lp13Dx6{$Offw@T#M-pAr~q=H6EqSo3SI-DOzFE82cs5IE4%Q zQJKf=_m||RPB+o@D?aRP645%?{aS#wIe}pWGA3=;&P(JWwkO=TCra$dyGxMjpAdD9`U=)e+E27_TN8_W@n!1YGrS>n$z zY@c_}(vYk^5}SB)Qe8G&SZL@~u%pAi4`=#qHJ&A0C>31;GW0+ev& zoT+7>(L);|haC4CO0a(8sXfCv!0(NoQdfnG;2$bMB1 ze@Q*eZODywUP8o9!VuDXCXBLlf+@w~JH!K+0Q}y&*{RYSk}KoiGH=&6&M0|6F>o8- zxZySkci?B%4EaS!yPf-}O*J#BH7u*-QN7m}hZ26!GH~oaERuTF0(W|7af_@pFqzZx zqyDYnIM&6%WIgDYV4uh=(sW>gt*16f`#teDBpEABQ*299zACpnt2F9E+m**SFVvZ6 z1jTl(XM=q6KW;VdQKnY?5TrsDpg*F%0mm%e*2bmn!})9~#!al2 zxmcFS20W`Wly3Gt7-c`*I zl?_yD)8q8-n80yjOIuVb0!7Qx4m z>t{#vP%T>d%%)eMHYF zHk($nbR`Y(-WCp!BJH-_^Z4GD0<3Njwj_Ez4ydySh9i<2E}id#Aq-~U7P~SgCuO0) zmcmrqADp7={Fj^{w-HJ@hOEvnuX<(_9!V zpal433(gloDJ`?5Zx}_T9KIVC@t#XVC1-LJM<~Q(?h?q-aOKWSxgG%wxytPY7VLO8w!f z=|ZEFTTO4jI)Js9dzf(_L9PK5Xs);9(w5S+IcVXmBu5^6ds^$n743)2LkVG` z`CDzN_$aP;+A&S#oaZyDkhGqgh5bzd>44WL9)tm}{Vzi651YU%9x6QuQ_(4J_&aJy zl99IeTSgi+5VV|%VROEHsm#%TU6&cib?gD-G}Wc(EYPi8jGpGybYqqnULo#63nMq| zSPK$Sz7*QZubs_lQHi1cN7}>}y`4FL%wSMvDdYJaB!B|2jQY@r1t`h><64(ysY;C@ z?ql2MHRi9=7E;#1Q)woE`c>-lwK{}0%x|zvoh3|zmV$@hd>IykNJwXpW*;T_Y<`F< zH?w4=5N3*=kcUkiCpJYSpu;0&e}LEj*SD|VgHUEyq%STsB6=M<>WU_0DaF}EnRjfd zt}Z}x+oG--%mZfTW^pV|(+FOS{*ya)ufivR{0uEh0ip_K6s5ytlZM)`U$w`5P_4X& z+fgAJ60qxh1L9eroHyn>ZX#-bshFg1km?0_@lP&?UY3($b3pN`+IZw1FI~V2ZKo}k z(6X5E;hyxwFf1E2aH(9D>1~79(n5)}AFD%)%zb&qxqc{al0OJ3=1c8jjQKiH=`q31v!A}dX4{hE2 z0#`PoW!XswwT;>1i0(0=%UO3?hGZ(x`$WEp2BoZYs)2+gv?Z_$+tf;Ud$ulr{>mB| zPL&0_4+bh+NR5Cyp!gee&lN@Qu$68y#lYYyy>T`~l7Is_0F@}Ws~TJC)d*C-jQP3@ z~2UR#r%rvcKsnRbJcVb+32(Gg6STk+&(w3ZhOqr!~LNvWvya zq|`fc00KNb1d?6iBNYSw_#YDXKu>=EqW2=cv~`~Oi_;IO2GPLQ@)bJAczyKy2T1Qj zaV}0HdeO4%Fd1L0e`0oPNQ|)U#HqoP(fnBrjs#Hu&M<1>4%Z7O&=9+GP~4e7jHf~| zqcd=}ea@7t9eE7tZ~Axl5fW5A19I0vijruiAa=f%-*a*so=^W{iyougK~jW1>+&%~ z4m{&Na8@@5hR7$RV#?D0_K!SD2H-J+%D6<5)P~Il6%HPt8o1@WRxSpB{;zk=u=A3t zIfZ&?&`j@!x4A#uT2Uc&cjweB<}sc3un>xkbcHxqn4a{(iMP$RB4@&sTGPvz(Fhaa z;zcmqVVeKpX;m)iVN$%gjf&x0i&wJ4LX~g^yfRQC3(plP%recpNfRu^2g$gEPc ze|sd3gYsWcOzNf})t(;HJfl3_xaUT2kA-E7tdg-*rljI|zF}#u+r8o-+(DsZkb;%! zo3SwQe7`&%xCq_{G51NYV%s=cu%kY(*3Aubc2+F!DH)NIW# z=T-nk=86;NseDnRU?CJdeuQUEV@T~|33~|vzR+i#l2vr%F zJAlSRWX0?^PAug;xXw*H5gE^olAftY=GkJYtaw?0-2s#zEOF_)CIDum#nCn+n09=y zjTbg}DlGIxoFv0K;?%DVvk58m|AdIy`!Qt6vl+Q%kR&W+NpFa>cJN+I?$IRjqm^W~ zFM87sfuvw^S>}e2 z_IB=tTX6Wcqu(CBW5ZhQ(GroV!2pRQUU|bZ|9m!MlPF1Mg+v8RNE@2B75mBAm~=7{ zj2Gl_;6F%&KEp4g3tPqcS4Zp|cPK8#^eMi$jUG_@2WEpLY*aU+-k^IJ8I&gCRX_>! z7m_rNFDQHAQnGw-x{712?GF2j&7#DQv!+}S2S}`+J$)veBfR3(fUbGH z|DNeiw|_*U=zFC|$*6al&jUKY?5olcHSe#h+d)~vCRrP}>{PZPSQMa_HJEZ>bBdjVxQ-|O2$Ky zXfFJN>=bMl*x}hJ&aF7iV*))q-P63V*8{ONi{!~?q8;4OGzuzQX2%P4+S;KGm^!cc z>eh=zrWX`ZELr=2U>c$kTz+1JuNZXQAQCCAU7S;w*N+j$l~Rqh@?)!=ep;UyzN%Ee z>VXx3#0TZ!`_J(|f?S2>xv^YuC?-}Zs<0V}NglTwttVog`}>~pE2jbvx%5Z$WvF*D zO$g;?y&>qSq_PZ$hT%bu%A;Vc?OJ*N?evD?cW&4HXw+S`ukrbIX@}!;^Z@1V*UP#u zPD?ycc$<%J^cDlo*x}(1TEZ41d9{Yf<2n^wZaFf_8j>uy(gQASw+;3g=aOHCgp=Z( zp=wdK`j6}UOL_6+H*f!={(_~Qr$=hWZFk=Z*|0v~Y(W6e=xtjN@#VeL*Fp za&P6Qp!Ch~eaIYeCn;FjTz+`=lU09FM}iOr2w=9%#X}r-;-gB>a@K&kXoj3i>p$tp z2xYnzVR+s)pZ7AQw#VFDk?P)Pw;-QN&qNnJ!+IANJ}$s!4O@SY0ixl5f?})Vm$35| zXTc=?C;o4F^;8sK;ysVL(lW=LoQybi4NVaLJkfvXdDwIR-@G_383et4CljreZQ0ja zai#uNwMF)>(HNpEpR>BHe?hPaCMp7I(2h%VfQ;M60{=EhdZ#FNrwnrkviPSqmV^fn zR72&!d_u{(oVRnP*y6M|n;GV$SZUQmYQezn(^ZBU0^g4-2n1Dx%bdgIeCwUPcjUn5 z<7ia>(@5`@GWLx&Kq1T8Jm2MD#Z}=YJE5WOVxSeE(7=TLv)9TT!FP9fQB8=x(yIpajzSD!TK%u7gD9Wm}H=TJ>D_;%DraI z>-)T;q$gudLN`ZWg^jPdqk~65{<&!}iKZ=ixl<8C!8oAqCAPwYvTs|npDA$`NY`_rQ;$q zvTr^5_JAkJyMM15%TO-EuhSOAtwYQ4)5Z@yf;QZ0%|a_~XzqVzUH?zIZvs__xVx3) z@gQ&LrO4)B2V&r)C_th{bbc~X%r z+R9$W(p*;llIB<1BV=_FM+97!o+d%lz4M`(w_46mOcghECLZHn%#MJ#@}2SJc7Q|< zWjiQ8zAtWPl*vZUNd@6M@I6wAlPrk^s*ko~y10`!9XV}(&a89rTFKSv`22@B!5Or}gQ}+#i z{@T<5uXVatrpqj1^e=&p@v0-`Ar^5@`ixA(#fzFBFQa`76%YazqJbW0i0;SR&4p;@eSe_l$7vQYz6^ z{LWQYrdRO^RdCtUg{;guLe3rd9%@VZVt~ve&;9oT=8sM&2JmKs6ULiGsBmVkB-=ch z`%IWZ`geLTii(8N%Al`7?9TE|Y;F;b@1$7}pu;AuTl8k&yY*_RE6Uu}zRS4q3y()L zAWoo`%)Xs2-V1WmOf}S^sJq@F=Ml1XaVBUiV*Cj4r@Ps?I)EUgFx1Asmb?nec{6JY zJ4m%Jqz?p!6}gw>dl zZ7airKLQr8>KJ}x{?=&=uF~BfFhTFSi#V`7`E#CQT5rwkY0b9M-4Lui*QfflG1g2x zaW&qI{c;kw`2lx#@i^rszn*;AJ`=&BdaW}6scR>2 zwVuL75dj6u)wO5yUl?DBOjAD-?G#f#eaCwzh zmqq9HN$z$E%K&PGA~Y8*LN=UnjVI^+!@E;wsh&w~utr^pkt(@`V6-^MqfY?JNEbT@ z!AY$?B!Zz3g><`)z2SWiQ0S7m=wT9UOH{sVNs0T(H8dGRU6NV>2uMNltj_0q4NV3F zqyQ^R`k|iS|I>FV_GgYZ&hgz**)H)D1E9d4 zA&VE0AAq5325gr@E)P)3uR3ZMT-{2rg5u`k;M(k?GQ3_Bc=Akd|K|e0r)QqdAl7FC z5Z{+Ft#1ph#`OixFh=I)SL2K3Q4z7&E(h@$ygr9EX3pAh%yx{#=<)j&K+j4@Yb=#;uU^#=Jg!(PLZJXkHSCOMo0pvVMF zc1Wq#_gkdc2Qu9odz$$Og5;Sk;Iwgbr2CSSCS3$u>oD7%S4?PXzPvIeL7z8*hDZ)C zu~_Jsz9DEhMFy z9kw12nG{`_rOKvMeOvTT)7ebBb$#=9RBC<41ar}qUu?li04}4l7Xx3Q7N<8V5Km~x zjT@OZ?itd_QmT!x(gxc{(cLLgeGZ09!q6t94;iaMk;U`7Fm=I)^KnYB0 zA1428n7<{rfCcilTWkZWP%WFe4WC@oY3xXYzGc}DRQ5Mi&$?e zP0V^!$fuT<5<@xJdoMAGWLaomRH)!V+wN{8eI`Ef?@edO0})BplV!7XITQ`(^f3o} zBVK9t`iKM&dq%zTJ-ON9f?z}SMn3r?v_4wI6%?1(%d3XZXLgQ(DS1dy#l&WZbvVe( zp@Zx+_Vs*Bo4uHX%3k!UGcI=CWb&VjEKKDh7Xq_Z>L2|sCvl$Vb)a0XDQAj1#&2H% zjrQT?|1YStopJw@TsV)YAEDx#X=HI*gNB*>@Pa81t{nG$RtnwD_N~Z>Q4tj4Olz7} zQ5fT8Bhe!8-rw)ParpX&pHF;dLGcQar{Og7xrFp*7GKFH=4!mIIvrujp%FzOLYUX} zX?{gI&eDDPc$yKF5-6>vhOIiI!Qfuew7N@1d=kLXzQm&AkR9*>eWRsn;H)kXE>3CN zy%YwRA*TD%ftv*ONgF>>V`~bwW?OHTxarb!hZ0QCgVH{z=}|_bvKGY&G)YbvBJ3I> zf&D{{6t+J51THlh)(|LxMnJdxeBp3$zdUzeZtC*pK+I9DDQ|Vd(aaDx`O;bDes!t^ zsMS~vMTonGO(gpFwQHgLD8o)DRAB-b;)CWnzAV4ZI%wv{V)MaX<@r#lqx{X~{W(Lc cfIe1LT%n%w2rF6vyeVX-F$RvKf6FXe=+ef~uK)l5 literal 0 HcmV?d00001