pax_global_header00006660000000000000000000000064145474734110014524gustar00rootroot0000000000000052 comment=0dfd5596efaf42af3a8debf0919358197b0721e6 fakemachine-0.0.9/000077500000000000000000000000001454747341100137655ustar00rootroot00000000000000fakemachine-0.0.9/.github/000077500000000000000000000000001454747341100153255ustar00rootroot00000000000000fakemachine-0.0.9/.github/dependabot.yml000066400000000000000000000003151454747341100201540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" fakemachine-0.0.9/.github/workflows/000077500000000000000000000000001454747341100173625ustar00rootroot00000000000000fakemachine-0.0.9/.github/workflows/ci.yml000066400000000000000000000053401454747341100205020ustar00rootroot00000000000000name: Build and Test on: push: branches-ignore: - '*.tmp' # Build at 04:00am every Monday schedule: - cron: "0 4 * * 1" pull_request: workflow_dispatch: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 - uses: actions/checkout@v4 - 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@v4 - 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 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: [bookworm, trixie] backend: [qemu, uml, kvm] include: - os: arch backend: "qemu" - os: arch backend: "kvm" name: Test ${{matrix.os}} with ${{matrix.backend}} backend runs-on: ${{ matrix.backend == 'kvm' && 'kvm' || 'ubuntu-latest' }} defaults: run: shell: bash container: 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: - name: Checkout code uses: actions/checkout@v4 - 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 success status against allgreen: name: allgreen if: always() needs: - golangci - man-page - test runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} fakemachine-0.0.9/.gitignore000066400000000000000000000000231454747341100157500ustar00rootroot00000000000000/fakemachine .*swp fakemachine-0.0.9/.golangci.yml000066400000000000000000000001211454747341100163430ustar00rootroot00000000000000linters: enable: - errorlint - gofmt - stylecheck - whitespace fakemachine-0.0.9/Dockerfile000066400000000000000000000014361454747341100157630ustar00rootroot00000000000000FROM debian:stretch-slim ARG DEBIAN_FRONTEND=noninteractive ENV HOME=/scratch # Always install procps in case the docker file gets used in jenkins RUN apt update && apt-get install --no-install-recommends -y procps # Bits needed to run fakemachine RUN apt-get update && \ apt-get install --no-install-recommends -y qemu-system-x86 \ qemu-user-static \ busybox \ linux-image-amd64 \ systemd \ dbus # Bits needed to build fakemachine RUN apt-get update && \ apt-get install --no-install-recommends -y golang-go git ca-certificates WORKDIR /scratch fakemachine-0.0.9/LICENSE000066400000000000000000000261251454747341100150000ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017,2018 Collabora Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. fakemachine-0.0.9/README.md000066400000000000000000000017331454747341100152500ustar00rootroot00000000000000# 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 ``` fakemachine-0.0.9/backend.go000066400000000000000000000060231454747341100157040ustar00rootroot00000000000000//go:build linux // +build linux package fakemachine import ( "fmt" ) // 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 { 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 var err 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 { /* 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 } return nil, err } // find backend by name for _, backend := range backends { if backend.Name() == name { b = backend } } if b == nil { return nil, fmt.Errorf("%s backend does not exist", name) } // check backend is supported if supported, err := b.Supported(); !supported { return nil, fmt.Errorf("%s backend not supported: %w", name, err) } return b, nil } type backend interface { // The name of the backend Name() string // Whether the backend is supported on this machine; if the backend is // not supported then the error contains a user-facing reason Supported() (bool, error) // Get kernel release version KernelRelease() (string, error) // The path to the kernel KernelPath() (kernelPath string, err error) // The path to the modules ModulePath() (moddir string, err error) // A list of udev rules UdevRules() []string // The tty used for the job output JobOutputTTY() string // The parameters used to mount a specific volume into the machine MountParameters(mount mountPoint) (fstype string, options []string) // 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 InitStaticVolumes() []mountPoint // Start an instance of the backend Start() (bool, error) } fakemachine-0.0.9/backend_qemu.go000066400000000000000000000176071454747341100167450ustar00rootroot00000000000000//go:build linux && (arm64 || amd64) package fakemachine import ( "bytes" "fmt" "io/ioutil" "os" "os/exec" "path" "strings" "golang.org/x/sys/unix" ) type qemuBackend struct { machine *Machine } func newQemuBackend(m *Machine) qemuBackend { return qemuBackend{machine: m} } func (b qemuBackend) Name() string { return "qemu" } func (b qemuBackend) Supported() (bool, error) { if _, err := b.QemuPath(); err != nil { return false, err } 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) { 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) { /* 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 * environments */ var u unix.Utsname if err := unix.Uname(&u); err != nil { return "", err } release := string(u.Release[:bytes.IndexByte(u.Release[:], 0)]) if _, err := os.Stat(path.Join("/lib/modules", release)); err == nil { return release, nil } files, err := ioutil.ReadDir("/lib/modules") if err != nil { return "", err } 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() if filename[0] >= '0' && filename[0] <= '9' { return filename, nil } } return "", fmt.Errorf("kernel not found") } 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 } } /* Fall-back to the previous method and look in /boot */ kernelRelease, err := b.KernelRelease() if err != nil { return "", err } kernelPath := "/boot/vmlinuz-" + kernelRelease if _, err := os.Stat(kernelPath); err != nil { return "", err } return kernelPath, nil } func (b qemuBackend) ModulePath() (string, error) { kernelRelease, err := b.KernelRelease() if err != nil { return "", err } moddir := "/lib/modules" if mergedUsrSystem() { moddir = "/usr/lib/modules" } moddir = path.Join(moddir, kernelRelease) if _, err := os.Stat(moddir); err != nil { return "", err } return moddir, nil } func (b qemuBackend) UdevRules() []string { udevRules := []string{} // create symlink under /dev/disk/by-fakemachine-label/ for each virtual image for i, img := range b.machine.images { driveLetter := 'a' + i udevRules = append(udevRules, fmt.Sprintf(`KERNEL=="vd%c", SYMLINK+="disk/by-fakemachine-label/%s"`, driveLetter, img.label), fmt.Sprintf(`KERNEL=="vd%c[0-9]", SYMLINK+="disk/by-fakemachine-label/%s-part%%n"`, driveLetter, img.label)) } return udevRules } 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 // (which we also ignore). // If we are debugging, mix job output into the normal // console messages instead, so we can see both. if b.machine.showBoot { return "/dev/console" } return "/dev/hvc0" } func (b qemuBackend) MountParameters(mount mountPoint) (string, []string) { return "9p", []string{"trans=virtio", "version=9p2000.L", "cache=loose", "msize=262144"} } func (b qemuBackend) InitModules() []string { return []string{"virtio_pci", "virtio_console", "9pnet_virtio", "9p"} } func (b qemuBackend) InitStaticVolumes() []mountPoint { return []mountPoint{} } func (b qemuBackend) Start() (bool, error) { return b.StartQemu(false) } func (b qemuBackend) StartQemu(kvm bool) (bool, error) { m := b.machine qemuMachine := qemuMachines[m.arch] kernelPath, err := b.KernelPath() if err != nil { return false, err } memory := fmt.Sprintf("%d", m.memory) numcpus := fmt.Sprintf("%d", m.numcpus) qemuargs := []string{qemuMachine.binary, "-smp", numcpus, "-m", memory, "-kernel", kernelPath, "-initrd", m.initrdpath, "-display", "none", "-nic", "user,model=virtio-net-pci", "-no-reboot"} if kvm { qemuargs = append(qemuargs, "-cpu", "host", "-enable-kvm") } else if qemuMachine.qemuCPU != "" { qemuargs = append(qemuargs, "-cpu", qemuMachine.qemuCPU) } 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 { // Create a character device representing our stdio // file descriptors, and connect the emulated serial // port (which is the console device for the BIOS, // Linux and systemd, and is also connected to the // fakemachine script) to that device 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 "-device", "virtio-serial", // Create /dev/ttyS0 to be the VM console, but // ignore anything written to it, so that it // doesn't corrupt our terminal "-chardev", "null,id=for-ttyS0", "-serial", "chardev:for-ttyS0", // Connect the fakemachine script to our stdio // file descriptors "-chardev", "stdio,id=for-hvc0,signal=off", "-device", "virtconsole,chardev=for-hvc0") } for _, point := range m.mounts { qemuargs = append(qemuargs, "-virtfs", fmt.Sprintf("local,mount_tag=%s,path=%s,security_model=none,multidevs=remap", point.label, point.hostDirectory)) } for i, img := range m.images { qemuargs = append(qemuargs, "-drive", fmt.Sprintf("file=%s,if=none,format=raw,cache=unsafe,id=drive-virtio-disk%d", img.path, i)) qemuargs = append(qemuargs, "-device", fmt.Sprintf("virtio-blk-pci,drive=drive-virtio-disk%d,id=virtio-disk%d,serial=%s", i, i, img.label)) } qemuargs = append(qemuargs, "-append", strings.Join(kernelargs, " ")) pa := os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, } qemubin, err := b.QemuPath() if err != nil { return false, err } p, err := os.StartProcess(qemubin, qemuargs, &pa) if err != nil { return false, err } // wait for kvm process to exit pstate, err := p.Wait() if err != nil { return false, err } 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) } fakemachine-0.0.9/backend_uml.go000066400000000000000000000155011454747341100165620ustar00rootroot00000000000000//go:build linux // +build linux package fakemachine import ( "errors" "fmt" "io/ioutil" "os" "os/exec" "path" "golang.org/x/sys/unix" ) type umlBackend struct { machine *Machine } func newUmlBackend(m *Machine) umlBackend { return umlBackend{machine: m} } func (b umlBackend) Name() string { return "uml" } 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 } // check the modules exist if _, err := b.ModulePath(); err != nil { return false, err } // check the slirp helper exists exec.LookPath if _, err := b.SlirpHelperPath(); err != nil { return false, fmt.Errorf("libslirp-helper not installed") } return true, nil } func (b umlBackend) KernelRelease() (string, error) { return "", errors.New("not implemented") } 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 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") } // find the subdirectory containing the modules for the UML release modSubdirs, err := ioutil.ReadDir(moddir) if err != nil { return "", err } if len(modSubdirs) != 1 { return "", fmt.Errorf("could not determine which user-mode-linux modules to use") } moddir = path.Join(moddir, modSubdirs[0].Name()) return moddir, nil } func (b umlBackend) SlirpHelperPath() (string, error) { return exec.LookPath("libslirp-helper") } func (b umlBackend) UdevRules() []string { udevRules := []string{} // create symlink under /dev/disk/by-fakemachine-label/ for each virtual image for i, img := range b.machine.images { driveLetter := 'a' + i udevRules = append(udevRules, fmt.Sprintf(`KERNEL=="ubd%c", SYMLINK+="disk/by-fakemachine-label/%s"`, driveLetter, img.label), fmt.Sprintf(`KERNEL=="ubd%c[0-9]", SYMLINK+="disk/by-fakemachine-label/%s-part%%n"`, driveLetter, img.label)) } return udevRules } func (b umlBackend) JobOutputTTY() string { // Send the fakemachine job output to the right console if b.machine.showBoot { return "/dev/tty0" } return "/dev/tty1" } func (b umlBackend) MountParameters(mount mountPoint) (fstype string, options []string) { fstype = "hostfs" options = []string{mount.hostDirectory} return } func (b umlBackend) InitModules() []string { return []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.ModulePath() moddir = path.Join(moddir, "../") machineDir := "/lib/modules" if mergedUsrSystem() { machineDir = "/usr/lib/modules" } moduleVolume := mountPoint{moddir, machineDir, "modules", true} return []mountPoint{moduleVolume} } func (b umlBackend) Start() (bool, error) { m := b.machine kernelPath, err := b.KernelPath() if err != nil { return false, err } slirpHelperPath, err := b.SlirpHelperPath() if err != nil { return false, err } /* for networking we use the UML vector transport alongside the * libslirp-helper on the host. This works by creating a pair of * connected sockets on the host using the socketpair syscall, which * returns two file descriptors. One of the sockets is attached to the * UML process and the other socket is attached to the libslirp-helper * process allowing communication between the two processes. * It doesn't matter the order in which the sockets are connected to * the processes. */ netSocketpair, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0) if err != nil { return false, err } // 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") } defer slirpHelperSocket.Close() // while the other socket will be attached to the uml guest umlVectorTransportSocket := os.NewFile(uintptr(netSocketpair[1]), "") if umlVectorTransportSocket == nil { return false, fmt.Errorf("creation of umlVectorTransportSocket failed") } defer umlVectorTransportSocket.Close() // launch libslirp-helper slirpHelperArgs := []string{"libslirp-helper", "--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 * file descriptor using the --fd argument. Since the standard std* * file descriptors are passed to the libslirp-helper --fd should * always be set to 3. */ slirpHelperAttr := &os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr, slirpHelperSocket}, } slirpHelperArgs = append(slirpHelperArgs, "--fd=3") slirpHelper, err := os.StartProcess(slirpHelperPath, slirpHelperArgs, slirpHelperAttr) if err != nil { return false, err } defer func() { _ = slirpHelper.Kill() }() // launch uml guest memory := fmt.Sprintf("%d", m.memory) umlargs := []string{"linux", "mem=" + memory + "M", "initrd=" + m.initrdpath, "panic=-1", "plymouth.enable=0", "systemd.unit=fakemachine.service", "console=tty0", } /* umlVectorTransportSocket is attached as an additional fd to the process, * after the std* file descriptors. Setup a vector device inside the guest * which uses fd transport with the 3rd file descriptor attached to the * UML process. */ umlAttr := &os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr, umlVectorTransportSocket}, } umlargs = append(umlargs, "vec0:transport=fd,fd=3,vec=0") if m.showBoot { // Create a character device representing our stdio // file descriptors, and connect the emulated serial // port (which is the console device for the BIOS, // Linux and systemd, and is also connected to the // fakemachine script) to that device umlargs = append(umlargs, "con0=fd:0,fd:1", // tty0 to stdin/stdout when showing boot "con=none") // no other consoles } else { // don't show the UML message output by default umlargs = append(umlargs, "quiet") umlargs = append(umlargs, "con1=fd:0,fd:1", "con0=null", "con=none") // no other consoles } for i, img := range m.images { umlargs = append(umlargs, fmt.Sprintf("ubd%d=%s", i, img.path)) } p, err := os.StartProcess(kernelPath, umlargs, umlAttr) if err != nil { return false, err } // wait for uml process to exit ustate, err := p.Wait() if err != nil { return false, err } return ustate.Success(), nil } fakemachine-0.0.9/cmd/000077500000000000000000000000001454747341100145305ustar00rootroot00000000000000fakemachine-0.0.9/cmd/fakemachine/000077500000000000000000000000001454747341100167635ustar00rootroot00000000000000fakemachine-0.0.9/cmd/fakemachine/main.go000066400000000000000000000123641454747341100202440ustar00rootroot00000000000000package main import ( "errors" "fmt" "github.com/alessio/shellescape" "github.com/docker/go-units" "github.com/go-debos/fakemachine" "github.com/jessevdk/go-flags" "os" "strings" ) 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"` 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"` 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 var parser = flags.NewParser(&options, flags.Default) func warnLocalhost(variable string, value string) { message := `WARNING: Environment variable %[1]s contains a reference to localhost. This may not work when running from fakemachine. 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") { fmt.Printf(message, variable) } } func SetupVolumes(m *fakemachine.Machine, options Options) { for _, v := range options.Volumes { parts := strings.Split(v, ":") switch len(parts) { case 1: m.AddVolume(parts[0]) case 2: m.AddVolumeAt(parts[0], parts[1]) default: fmt.Fprintln(os.Stderr, "Failed to parse volume:", v) os.Exit(1) } } } func SetupImages(m *fakemachine.Machine, options Options) { for _, i := range options.Images { parts := strings.Split(i, ":") var err error var l string switch len(parts) { case 1: l, err = m.CreateImage(parts[0], -1) case 2: var size int64 size, err = units.FromHumanSize(parts[1]) if err != nil { break } l, err = m.CreateImage(parts[0], size) default: fmt.Fprintf(os.Stderr, "Failed to parse image: %s\n", i) os.Exit(1) } if err != nil { fmt.Fprintf(os.Stderr, "Failed to create image: %s %v\n", i, err) os.Exit(1) } if !options.Quiet { fmt.Printf("Exposing %s as %s\n", parts[0], l) } } } func SetupEnviron(m *fakemachine.Machine, options Options) { // Initialize environment variables map EnvironVars := make(map[string]string) // 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 environVars = [...]string{ "http_proxy", "https_proxy", "ftp_proxy", "rsync_proxy", "all_proxy", "no_proxy", } // First add variables from host for _, e := range environVars { lowerVar := strings.ToLower(e) // lowercase not really needed lowerVal := os.Getenv(lowerVar) if lowerVal != "" { EnvironVars[lowerVar] = lowerVal } upperVar := strings.ToUpper(e) upperVal := os.Getenv(upperVar) if upperVal != "" { EnvironVars[upperVar] = upperVal } } // Then add/overwrite with variables from command line for k, v := range options.EnvironVars { // Allows the user to unset environ variables with -e if v == "" { delete(EnvironVars, k) } else { EnvironVars[k] = v } } // Puts in a format that is compatible with output of os.Environ() 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() { // append the list of available backends to the commandline argument parser opt := parser.FindOptionByLongName("backend") opt.Choices = fakemachine.BackendNames() args, err := parser.Parse() if err != nil { var flagsErr *flags.Error if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp { os.Exit(0) } else { os.Exit(1) } } m, err := fakemachine.NewMachineWithBackend(options.Backend) if err != nil { fmt.Printf("fakemachine: %v\n", err) os.Exit(1) } m.SetShowBoot(options.ShowBoot) m.SetQuiet(options.Quiet) SetupVolumes(m, options) SetupImages(m, options) SetupEnviron(m, options) if options.ScratchSize != "" { size, err := units.FromHumanSize(options.ScratchSize) if err != nil { fmt.Fprintf(os.Stderr, "fakemachine: Couldn't parse scratch size: %v\n", err) os.Exit(1) } m.SetScratch(size, "") } if options.Memory > 0 { m.SetMemory(options.Memory) } if options.CPUs > 0 { m.SetNumCPUs(options.CPUs) } command := "/bin/bash" if len(args) > 0 { command = shellescape.QuoteCommand(args) } ret, err := m.Run(command) if err != nil { fmt.Fprintf(os.Stderr, "fakemachine: %v\n", err) } os.Exit(ret) } fakemachine-0.0.9/cpio/000077500000000000000000000000001454747341100147175ustar00rootroot00000000000000fakemachine-0.0.9/cpio/writerhelper.go000066400000000000000000000112751454747341100177700ustar00rootroot00000000000000package writerhelper import ( "bytes" "fmt" "io" "os" "path" "path/filepath" "strings" "github.com/surma/gocpio" ) type WriterHelper struct { paths map[string]bool *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}, Writer: cpio.NewWriter(f), } } func (w *WriterHelper) ensureBaseDirectory(directory string) error { d := path.Clean(directory) if w.paths[d] { return nil } components := strings.Split(directory, "/") collector := "/" for _, c := range components { collector = path.Join(collector, c) if w.paths[collector] { continue } 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) error { err := w.ensureBaseDirectory(path.Dir(directory)) if err != nil { return err } hdr := new(cpio.Header) hdr.Type = cpio.TYPE_DIR hdr.Name = directory hdr.Mode = int64(perm) 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) error { return w.WriteFileRaw(file, []byte(content), perm) } 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) hdr.Type = cpio.TYPE_REG hdr.Name = file hdr.Mode = int64(perm) hdr.Size = int64(len(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) error { err := w.ensureBaseDirectory(path.Dir(link)) if err != nil { return err } hdr := new(cpio.Header) content := []byte(target) hdr.Type = cpio.TYPE_SYMLINK hdr.Name = link hdr.Mode = int64(perm) hdr.Size = int64(len(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) error { err := w.ensureBaseDirectory(path.Dir(device)) if err != nil { return err } hdr := new(cpio.Header) hdr.Type = cpio.TYPE_CHAR hdr.Name = device hdr.Mode = int64(perm) hdr.Devmajor = major hdr.Devminor = minor err = w.WriteHeader(hdr) if err != nil { return err } return nil } func (w *WriterHelper) CopyTree(path string) error { walker := func(p string, info os.FileInfo, _ error) error { var err error if info.Mode().IsDir() { err = w.WriteDirectory(p, info.Mode() & ^os.ModeType) } else if info.Mode().IsRegular() { err = w.CopyFile(p) } else { err = fmt.Errorf("file type not handled for %s", p) } return err } return filepath.Walk(path, walker) } func (w *WriterHelper) CopyFileTo(src, dst string) error { err := w.ensureBaseDirectory(path.Dir(dst)) if err != nil { return err } f, err := os.Open(src) if err != nil { return fmt.Errorf("open failed: %s - %w", src, err) } defer f.Close() info, err := f.Stat() 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 = info.Size() 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 } func (w *WriterHelper) CopyFile(in string) error { return w.CopyFileTo(in, in) } fakemachine-0.0.9/decompressors.go000066400000000000000000000016631454747341100172120ustar00rootroot00000000000000package 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 } fakemachine-0.0.9/decompressors_test.go000066400000000000000000000035231454747341100202460ustar00rootroot00000000000000package 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 } checkFile, err := os.Open(path.Join("testdata", file)) if err != nil { t.Errorf("Unable to open check data: %s", err) return } defer checkFile.Close() err = checkStreamsMatch(t, output, checkFile) 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) } fakemachine-0.0.9/doc/000077500000000000000000000000001454747341100145325ustar00rootroot00000000000000fakemachine-0.0.9/doc/man/000077500000000000000000000000001454747341100153055ustar00rootroot00000000000000fakemachine-0.0.9/doc/man/create_manpage.sh000077500000000000000000000007751454747341100206100ustar00rootroot00000000000000#!/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 fakemachine-0.0.9/doc/man/fakemachine.1000066400000000000000000000025231454747341100176240ustar00rootroot00000000000000.\" 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 fakemachine-0.0.9/doc/man/fakemachine.md000066400000000000000000000017631454747341100200710ustar00rootroot00000000000000% 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 ``` fakemachine-0.0.9/go.mod000066400000000000000000000005221454747341100150720ustar00rootroot00000000000000module 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.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.16.0 ) fakemachine-0.0.9/go.sum000066400000000000000000000053761454747341100151330ustar00rootroot00000000000000github.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= 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.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= 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/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.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.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= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= fakemachine-0.0.9/machine.go000066400000000000000000000547051454747341100157330ustar00rootroot00000000000000//go:build linux && (arm64 || amd64) package fakemachine import ( "bufio" "bytes" "errors" "fmt" "github.com/alessio/shellescape" "io/ioutil" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "text/template" writerhelper "github.com/go-debos/fakemachine/cpio" ) func mergedUsrSystem() bool { f, _ := os.Lstat("/bin") return (f.Mode() & os.ModeSymlink) == os.ModeSymlink } // Parse modinfo output and return the value of module attributes // There may be multiple row with same fieldname so []string // is used to return all data. func getModData(modname string, fieldname string, kernelRelease string) []string { out, err := exec.Command("modinfo", "-k", kernelRelease, modname).Output() if err != nil { return nil } var fieldValue []string scanner := bufio.NewScanner(strings.NewReader(string(out))) scanner.Split(bufio.ScanLines) for scanner.Scan() { field := strings.Split(strings.TrimSpace(scanner.Text()), ":") if strings.TrimSpace(field[0]) == fieldname { fieldValue = append(fieldValue, strings.TrimSpace(field[1])) } } return fieldValue } // Get full path of module func getModPath(modname string, kernelRelease string) string { path := getModData(modname, "filename", kernelRelease) if len(path) != 0 { return path[0] } return "" } // Get all dependent module func getModDepends(modname string, kernelRelease string) []string { deplist := getModData(modname, "depends", kernelRelease) var modlist []string for _, v := range deplist { if v != "" { modlist = append(modlist, strings.Split(v, ",")...) } } // 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 } 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) if modpath == "" { return errors.New("modules path couldn't be determined") } if modpath == "(builtin)" || copiedModules[modname] { return nil } prefix := "" if mergedUsrSystem() { prefix = "/usr" } 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 deplist := getModDepends(modname, release) for _, mod := range deplist { if err := m.copyModules(w, mod, copiedModules); err != nil { return err } } return nil } // Evaluate any symbolic link, then return the path's directory. Returns an // absolute path. Think of it as realpath(1) + dirname(1) in bash. func realDir(path string) (string, error) { var p string var err error if p, err = filepath.Abs(path); err != nil { return "", err } if p, err = filepath.EvalSymlinks(p); err != nil { return "", err } 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 label string static bool } type image struct { path string label string } type Machine struct { arch Arch backend backend mounts []mountPoint count int images []image memory int numcpus int showBoot bool quiet bool Environ []string scratchsize int64 scratchpath string scratchfile string scratchdev string initrdpath string } // Create a new machine object with the auto backend func NewMachine() (*Machine, error) { return NewMachineWithBackend("auto") } // Create a new machine object 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 } // usr is mounted by specific label via /init m.addStaticVolume("/usr", "usr") if !mergedUsrSystem() { m.addStaticVolume("/sbin", "sbin") m.addStaticVolume("/bin", "bin") m.addStaticVolume("/lib", "lib") } // Mounts for ssl certificates if _, err := os.Stat("/etc/ca-certificates"); err == nil { m.AddVolume("/etc/ca-certificates") } if _, err := os.Stat("/etc/ssl"); err == nil { 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") } // Debian alternative symlinks if _, err := os.Stat("/etc/alternatives"); err == nil { m.AddVolume("/etc/alternatives") } // Debians binfmt registry if _, err := os.Stat("/var/lib/binfmts"); err == nil { m.AddVolume("/var/lib/binfmts") } return m, nil } func InMachine() (ret bool) { _, ret = os.LookupEnv("IN_FAKE_MACHINE") return } // Check whether the auto backend is supported func Supported() bool { _, err := newBackend("auto", nil) return err == nil } const initScript = `#!/bin/busybox sh busybox mount -t proc proc /proc busybox mount -t sysfs none /sys # probe additional modules {{ range $m := .Backend.InitModules }} busybox modprobe {{ $m }} {{ end }} # mount static volumes {{ range $point := StaticVolumes .Machine }} {{ MountVolume $.Backend $point }} {{ end }} exec /lib/systemd/systemd ` const networkdTemplate = ` [Match] Type=ether [Network] DHCP=ipv4 # Disable link-local address to speedup boot 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 --interface=ethernet0 if [ $? != 0 ]; then echo "WARNING: Network setup failed" echo "== Journal ==" journalctl -a --no-pager echo "== networkd ==" networkctl status networkctl list echo 1 > /run/fakemachine/result exit fi %[1]s echo $? > /run/fakemachine/result ` // The line 'Environment=%[2]s' is used for environment variables optionally // configured using Machine.SetEnviron() const serviceTemplate = ` [Unit] Description=fakemachine runner Conflicts=shutdown.target Before=shutdown.target Requires=basic.target Wants=systemd-resolved.service binfmt-support.service systemd-networkd.service After=basic.target systemd-resolved.service binfmt-support.service systemd-networkd.service OnFailure=poweroff.target [Service] Environment=HOME=/root IN_FAKE_MACHINE=yes %[2]s WorkingDirectory=-/scratch ExecStart=/wrapper ExecStopPost=/bin/sync ExecStopPost=/bin/systemctl poweroff -q -ff Type=idle TTYPath=%[1]s StandardInput=tty-force StandardOutput=inherit StandardError=inherit KillMode=process IgnoreSIGPIPE=no SendSIGHUP=yes LimitNOFILE=4096 ` // helper function to generate a mount command for a given mountpoint func tmplMountVolume(b backend, m mountPoint) string { fsType, options := b.MountParameters(m) mntCommand := []string{"busybox", "mount", "-v"} mntCommand = append(mntCommand, "-t", fsType) if len(options) > 0 { mntCommand = append(mntCommand, "-o", strings.Join(options, ",")) } mntCommand = append(mntCommand, m.label) mntCommand = append(mntCommand, m.machineDirectory) return strings.Join(mntCommand, " ") } // helper function to return the static volumes from a machine, since the mounts variable is unexported // include the extra static mounts from the backend func tmplStaticVolumes(m Machine) []mountPoint { mounts := []mountPoint{} for _, mount := range append(m.mounts, m.backend.InitStaticVolumes()...) { if mount.static { mounts = append(mounts, mount) } } return mounts } func executeInitScriptTemplate(m *Machine, b backend) ([]byte, error) { helperFuncs := template.FuncMap{ "MountVolume": tmplMountVolume, "StaticVolumes": tmplStaticVolumes, } type templateVars struct { Machine *Machine Backend backend } tmplVariables := templateVars{m, b} tmpl := template.Must(template.New("init").Funcs(helperFuncs).Parse(initScript)) out := &bytes.Buffer{} if err := tmpl.Execute(out, tmplVariables); err != nil { return nil, err } return out.Bytes(), nil } func (m *Machine) addStaticVolume(directory, label string) { m.mounts = append(m.mounts, mountPoint{directory, directory, label, true}) } // AddVolumeAt mounts hostDirectory from the host at machineDirectory in the // fake machine func (m *Machine) AddVolumeAt(hostDirectory, machineDirectory string) { label := fmt.Sprintf("virtfs-%d", m.count) for _, mount := range m.mounts { if mount.hostDirectory == hostDirectory && mount.machineDirectory == machineDirectory { // Do not need to add already existing mount return } } m.mounts = append(m.mounts, mountPoint{hostDirectory, machineDirectory, label, false}) m.count = m.count + 1 } // AddVolume mounts directory from the host at the same location in the // fake machine func (m *Machine) AddVolume(directory string) { m.AddVolumeAt(directory, directory) } // CreateImageWithLabel creates an image file at path a given size and exposes // it in the fake machine using the given label as the serial id. If size is -1 // then the image should already exist and the size isn't modified. // // label needs to be less then 20 characters due to limitations from qemu // // The returned string is the device path of the new image as seen inside // fakemachine. func (m *Machine) CreateImageWithLabel(path string, size int64, label string) (string, error) { if size < 0 { _, err := os.Stat(path) if err != nil { return "", err } } if len(label) >= 20 { 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) } } i, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return "", err } if size >= 0 { err = i.Truncate(size) if err != nil { return "", err } } i.Close() m.images = append(m.images, image{path, label}) return fmt.Sprintf("/dev/disk/by-fakemachine-label/%s", label), nil } // CreateImage does the same as CreateImageWithLabel but lets the library pick // the label. func (m *Machine) CreateImage(imagepath string, size int64) (string, error) { label := fmt.Sprintf("fakedisk-%d", len(m.images)) return m.CreateImageWithLabel(imagepath, size, label) } // SetMemory sets the fakemachines amount of memory (in megabytes). Defaults to // 2048 MB func (m *Machine) SetMemory(memory int) { m.memory = memory } // SetNumCPUs sets the number of CPUs exposed to the fakemachine. Defaults to // the number of available cores in the system. func (m *Machine) SetNumCPUs(numcpus int) { m.numcpus = numcpus } // SetShowBoot sets whether to show boot/console messages from the fakemachine. 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 func (m *Machine) SetScratch(scratchsize int64, path string) { m.scratchsize = scratchsize if path == "" { m.scratchpath, _ = os.Getwd() } else { m.scratchpath = path } } func (m Machine) generateFstab(w *writerhelper.WriterHelper, backend backend) error { fstab := []string{"# Generated fstab file by fakemachine"} if m.scratchfile == "" { fstab = append(fstab, "none /scratch tmpfs size=95% 0 0") } else { fstab = append(fstab, fmt.Sprintf("%s /scratch ext4 defaults,relatime 0 0", m.scratchdev)) } for _, point := range m.mounts { fstype, options := backend.MountParameters(point) fstab = append(fstab, fmt.Sprintf("%s %s %s %s 0 0", point.label, point.machineDirectory, fstype, strings.Join(options, ","))) } fstab = append(fstab, "") 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.builtin", "modules.alias", "modules.symbols"} for _, v := range modfiles { if err := w.CopyFile(moddir + "/" + v); err != nil { return err } } copiedModules := make(map[string]bool) for _, modname := range modules { if err := m.copyModules(w, modname, copiedModules); err != nil { return err } } return m.generateModulesDep(w, moddir, copiedModules) } func (m *Machine) setupscratch() error { if m.scratchsize == 0 { return nil } tmpfile, err := ioutil.TempFile(m.scratchpath, "fake-scratch.img.") if err != nil { return err } m.scratchfile = tmpfile.Name() m.scratchdev, err = m.CreateImageWithLabel(tmpfile.Name(), m.scratchsize, "fake-scratch") if err != nil { return err } mkfs := exec.Command("mkfs.ext4", "-q", tmpfile.Name()) err = mkfs.Run() return err } func (m *Machine) cleanup() { if m.scratchfile != "" { os.Remove(m.scratchfile) } m.scratchfile = "" } // Start the machine running the given command and adding the extra content to // the cpio. Extracontent is a list of {source, dest} tuples func (m *Machine) startup(command string, extracontent [][2]string) (int, error) { defer m.cleanup() 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 } m.AddVolumeAt(tmpdir, "/run/fakemachine") defer os.RemoveAll(tmpdir) err = m.setupscratch() if err != nil { return -1, err } m.initrdpath = path.Join(tmpdir, "initramfs.cpio") f, err := os.OpenFile(m.initrdpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return -1, err } backend := m.backend kernelModuleDir, err := backend.ModulePath() if err != nil { return -1, err } w := writerhelper.NewWriterHelper(f) 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 } err = w.WriteSymlink("/run", "/var/run", 0755) if err != nil { return -1, err } if mergedUsrSystem() { 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}, {Target: "/usr/lib64", Link: "/lib64", Perm: 0755}, }) if err != nil { return -1, err } } else { err = w.WriteDirectories([]writerhelper.WriteDirectory{ {Directory: "/sbin", Perm: 0744}, {Directory: "/bin", Perm: 0755}, {Directory: "/lib", Perm: 0755}, }) if err != nil { return -1, err } } prefix := "" if mergedUsrSystem() { prefix = "/usr" } // search for busybox; in some distros it's located under /sbin busybox, err := exec.LookPath("busybox") if err != nil { return -1, err } err = w.CopyFileTo(busybox, prefix+"/bin/busybox") if err != nil { return -1, err } /* Ensure systemd-resolved is available */ if _, err := os.Stat("/lib/systemd/systemd-resolved"); err != nil { return -1, err } dynamicLinker := archDynamicLinker[m.arch] err = w.CopyFile(prefix + dynamicLinker) if err != nil { return -1, err } /* C libraries */ libraryDir, err := realDir(dynamicLinker) if err != nil { return -1, err } 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 } err = w.WriteCharDevice("/dev/console", 5, 1, 0700) if err != nil { return -1, err } // Linker configuration 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 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 } 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" err = w.WriteFile("/etc/udev/rules.d/61-fakemachine.rules", udevRules, 0444) if err != nil { return -1, err } err = w.WriteFile("/etc/systemd/network/ethernet.network", 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 } err = w.WriteSymlink( "/lib/systemd/resolv.conf", "/etc/resolv.conf", 0755) if err != nil { return -1, err } err = m.writerKernelModules(w, kernelModuleDir, backend.InitModules()) if err != nil { return -1, err } err = w.WriteFile("etc/systemd/system/fakemachine.service", fmt.Sprintf(serviceTemplate, backend.JobOutputTTY(), strings.Join(m.Environ, " ")), 0644) if err != nil { return -1, err } err = w.WriteSymlink( "/lib/systemd/system/serial-getty@ttyS0.service", "/dev/null", 0755) if err != nil { return -1, err } err = w.WriteFile("/wrapper", fmt.Sprintf(commandWrapper, command), 0755) if err != nil { return -1, err } init, err := executeInitScriptTemplate(m, backend) if err != nil { return -1, err } 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 { err = w.CopyFileTo(v[0], v[1]) if err != nil { return -1, err } } 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: %w", backend.Name(), err) } result, err := os.Open(path.Join(tmpdir, "result")) if err != nil { return -1, err } exitstr, _ := ioutil.ReadAll(result) exitcode, err := strconv.Atoi(strings.TrimSpace(string(exitstr))) if err != nil { return -1, err } return exitcode, nil } // Run creates the machine running the given command func (m *Machine) Run(command string) (int, error) { return m.startup(command, nil) } // RunInMachineWithArgs runs the caller binary inside the fakemachine with the // specified commandline arguments func (m *Machine) RunInMachineWithArgs(args []string) (int, error) { name := path.Join("/", path.Base(os.Args[0])) quotedArgs := shellescape.QuoteCommand(args) command := strings.Join([]string{name, quotedArgs}, " ") executable, err := exec.LookPath(os.Args[0]) if err != nil { return -1, fmt.Errorf("failed to find executable: %w", err) } return m.startup(command, [][2]string{{executable, name}}) } // RunInMachine runs the caller binary inside the fakemachine with the same // commandline arguments as the parent func (m *Machine) RunInMachine() (int, error) { return m.RunInMachineWithArgs(os.Args[1:]) } fakemachine-0.0.9/machine_test.go000066400000000000000000000124251454747341100167630ustar00rootroot00000000000000package fakemachine import ( "bufio" "flag" "io" "os" "strings" "testing" "github.com/stretchr/testify/require" ) 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 { machine, err := NewMachineWithBackend(backendName) require.Nil(t, err) machine.SetNumCPUs(2) return machine } func TestSuccessfulCommand(t *testing.T) { t.Parallel() m := CreateMachine(t) exitcode, _ := m.Run("ls /") if exitcode != 0 { t.Fatalf("Expected 0 but got %d", exitcode) } } func TestCommandNotFound(t *testing.T) { t.Parallel() m := CreateMachine(t) exitcode, _ := m.Run("/a/b/c /") if exitcode != 127 { t.Fatalf("Expected 127 but got %d", exitcode) } } func TestImage(t *testing.T) { t.Parallel() m := CreateMachine(t) _, err := m.CreateImage("test.img", 1024*1024) require.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) } } func AssertMount(t *testing.T, mountpoint, fstype string) { m, err := os.Open("/proc/self/mounts") require.Nil(t, err) mtab := bufio.NewReader(m) for { line, err := mtab.ReadString('\n') if err == io.EOF { require.Fail(t, "mountpoint not found") break } require.Nil(t, err) fields := strings.Fields(line) if fields[1] == mountpoint { require.Equal(t, fields[2], fstype) return } } } func TestScratchTmp(t *testing.T) { t.Parallel() if InMachine() { AssertMount(t, "/scratch", "tmpfs") return } m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchTmp"}) if exitcode != 0 { t.Fatalf("Test for tmpfs mount on scratch failed with %d", exitcode) } } func TestScratchDisk(t *testing.T) { t.Parallel() if InMachine() { AssertMount(t, "/scratch", "ext4") return } m := CreateMachine(t) m.SetScratch(1024*1024*1024, "") exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestScratchDisk"}) if exitcode != 0 { t.Fatalf("Test for device mount on scratch failed with %d", exitcode) } } func TestMemory(t *testing.T) { t.Parallel() m := CreateMachine(t) m.SetMemory(1024) // Nasty hack, this gets a chunk of shell script inserted in the wrapper script // which is not really what fakemachine expects but seems good enough for // testing command := ` MEM=$(grep MemTotal /proc/meminfo | awk ' { print $2 } ' ) # MemTotal is usable ram, not physical ram so accept a range if [ ${MEM} -lt 900000 -o ${MEM} -gt 1024000 ] ; then exit 1 fi ` exitcode, _ := m.Run(command) if exitcode != 0 { t.Fatalf("Test for set memory failed with %d", exitcode) } } func TestSpawnMachine(t *testing.T) { t.Parallel() if InMachine() { t.Log("Running in the machine") return } m := CreateMachine(t) exitcode, _ := m.RunInMachineWithArgs([]string{"-test.run", "TestSpawnMachine"}) if exitcode != 0 { t.Fatalf("Test for respawning in the machine failed failed with %d", exitcode) } } func TestImageLabel(t *testing.T) { t.Parallel() if InMachine() { t.Log("Running in the machine") devices := flag.Args() require.Equal(t, len(devices), 2, "Only expected two devices") autolabel := devices[0] labeled := devices[1] info, err := os.Stat(autolabel) require.Nil(t, err) require.Equal(t, info.Mode()&os.ModeType, os.ModeDevice, "Expected a device") info, err = os.Stat(labeled) 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) require.Nil(t, err) labeled, err := m.CreateImageWithLabel("test-labeled.img", 1024*1024, "test-labeled") 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) } 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) } } fakemachine-0.0.9/testdata/000077500000000000000000000000001454747341100155765ustar00rootroot00000000000000fakemachine-0.0.9/testdata/test000066400000000000000000000400001454747341100164720ustar00rootroot00000000000000 ,qҩqdTfDj[ Kg5 "lP?BKk.Fv(vX =}',_>2]eI߇CS{I;\~.7}}YW |_& iYDoEƀum'Ee'lӈJ81j1.+yUnhF0'C ?3as/XDM+!x:{b}rG2H|[G R5 վ:fڭ01Z̳DpX5+NtDcC^,eSKXxLޟYի{A[? G\rr&"C;qekDZeRK8Q:uM3EkN,v ~* r\6sl>궈Z/aQ34)oثPRy ٦q/Hvi1 ^A)Md{654דּ巈b)4>%amj 1 |rzcy_${TBow ^{oPj/`,?.~$~c䝈x}|Рf4)W<߹u);BV-_Uʈ$\m.ʧ[PPK pa oO0K,I3ɭ9`g"rNdtiU(锃̔Re"dBR!`Bl Y;`0=K;N[kj§^r|wfiE? jDҩ6djTO`L^K:HL\<5˚r;#H@\wf|Wm˙ˢXu=cW~V5]y I7$N#@مyO㾹Z+}4as3VDX{8 j"<Eo(1>I$QZ+?ȿ[2>\gmoBeqa beFp2߁[za{g,}do-O01HtGk%վ?߁dbVգVaSWT^vK& iZs޽;}gnSE[;QIQ.PwRےw2ncU7Jw+/S|JU7yrF H:`9RfL9R15y?1#/3+ "cp3yqQV'<[ sl`-"̧~g[PjFs`,ÆyV4祉ݭGN;$Ö x{bWvkÙ032b|wۏB,d&L ta/{gZߦH Nu/_U6}Z?*NMiE~oo9%I$W__uTMwo|t ,H@\ӂ*fL% Y&ԂC`_&[okxDSѩkq\[v^mn*b  H3^Ia2M9Qnf8KXR,q:uOA^ E\j✄/ 0~M&_cZ +K1~jsDis@4QW.(Zv݀̀k]* L:) uO.eG;R;ޏߵyN?ޙ$S`R6vzNLGWWćN.Jbq' ij%{NZg@&#Yf FERƛ81bssMؐ~Z"&М<pu,X):`PliB?  uf"P$p'Z}lFOZ\ڣGQ2u![?#KݗB`*66zGI!Go0; zDl&jY&֏:upXl71|>ˤg`ΩBx;o7ӭ΢cltPN##~>E'ś28FRp-=$(z㬰gOga;]Kq̤Cx3 xc*ɦJd ͤirqjK)&wI3r7kdmn;GP$N EP+5T t-bS ~RugD :^,FπMha@uoZ樺cd3oAқʧa%c!cpW/<h!|b 0{YƼoW^ыz<I"Jy@{q ` mz>FUʲ}aF$vu98D9t*ghgIC6иz؋nqڞ> _3 .߁}xTv4gIa%җ Z~[<euPE6>sR*;B/OrƗi:c+M"N` }b ]%ͼ26B`lw#{V`#?t#C%?:BfN唖'ym xqW#>Q]g\ KH冶& ΥFkeCxӀΨ6EKwB!S񄕜9Rjܙ'.KSMC?ҷm3[W.VLFz]{\ ٯ߷N k+Ǥ8O>z.>Ǐ 8!BikZzTHS:cʻQqU#dd0\3Rd1O2ӄGMSTDƧ5K~UR(.вH~Fӱ}d$[3ZE$!;?mk=w{޽>|lIxzQbDɜq:HXUZCw UTU(VE߻ns08nCwvnu`aU#߻Y8Q&Ӫ] >V:Wd,u@]AOj@)F Ҁ`Yj&¦tJ OVA2``zpRZH`OF|O3)89OZӀl)qOCy|`w`ajF Xl,̚]vSy7i*wYЃ~IOF:y?ڡ[zU>׷R6q!R}9q+q-ޓۈwJ %N<ʗd680 t:EP-02GM׀"o[HtЄwr= L]zz4?Jel(B?tPWA(.D Xbww=m<`Ncm.^)Pbsdc6}3"PCgʑccl’˺y;+1 K֑,1' Ti>lE=t p:̸n|+Ӱv;qw j`qrFRn(BO~n brfw6_$.xGW7ۤq@'jAk-ig.uUL%(.-\nǚz >K1]̏ǘB~ikD@BU@U.uu[Ec % oP* EJa=/(^KGPBLGk"m~GO@:9傧KB 4:'4mR&+%?RR3vC޲@7:dF>W[?2|-6CfWV^W2F]ze 5Lj &xKLEs쩪1a6@b"mX2.$eJFClk]{ǘζ@+Dil!:Wy|`*?s#367mĔ0OSL⯞S,JЌe8iMA.yyBg:$EqmbQP! *K"_j; ҂81/*,#b@#;Ҡ:|2Sxoi@q7g4wϭ$SUuv!B&q2|  L9%t^d4`|MSYv-'ٖ͝9nÛ\o#ˠ 9H "# .NN;|>YػMqBP~4Ri}1fEt *tdEN T(t.>;ZxU qdM22wAXN{+uU'?2DJ=2uNH5FiE~D-oU Gmκ(7.aJNfqױC TB0D&"2@Ȭ+u QvB Tbp*(x2- k ӁalnUw-!Ir*#wLbG\- gv330l @VqWpDo4eI]ˬ CMoHl\X <7>xH{a7}(! aT1O| mhlzYau?b& WfͶ{xe4Ri/Fs`0_RM47K;2bQiH9:| R4V|$|VP̫(ؕؒ |.98H11nF [l qy ӁަlZ]K`NwĉqO) j>DE} &nUGRCtȗX$Dq> Of&x=w4SdQOǵwLb2q)˧ bv1ȏ2r=D{?kKe)n(IjM)|ږ*bL1==\ !*=5͛#)Za%8 QN7+>m:Z9a2&mK!'ぇR9~iz[MR~r^ϸe'y!Ra,FY1lwҐ>xsR]6\XhUþg3yMxͷ+ fxp圩eC"X y}3bVwi,@@[A[W:q1˭5,/v!TcÊ9bT~_aL|6ME: FK9MdRL3 {1prwt€S͉^G0~UI=mȍv^BN{&t);޻٩+2o7(y@1p߸6wg EHM535,Q~2p?,zwzhq408d=`,t`6H=7$+MSlKS*;4Cە8/ԙhvg|?n5QVB?,ƥlx=SfqhS;g}*Vփg@ JcS6o gEnj ”)]fIQGeޮlˆu$_09]o.eQN>A:#GL;$DȻ;sml#>'+ =[SvVen٠sMzMr4"OQln#wk%47lj'W޷Vkp<=WYK*#@'d )"T#Sgup. '*[6Vt% "ޖn$z=#.! f2&'eT uY =Ӂ! HW &O&=,*|\,e A)-oEKC%grG(dpy Ru qU^ `͘}ޔdVT`z}WFJB[Mo:{qAhҖҴ9h$G]jFŖ> &oGb4{yӼ>􅙩ѩ}6{ZC a[m(x1M3M(/B+*=S)o;jH[F5as}]u4ե,Э]tc^!B #7X QʯiQG9`Pf)$Ї(ZfJ!c5RSi&U:6oL%L|HHgf%l~+7dfM'6E$#e׷߃PfW/.4"z:&d)Ewl.@sۢffq,N^;v' -J fdڰ_}PQ!|Y7;n"joyO.^bs@x/] m-в0,5,wwnot8/Y EdmtOM TP=;ɍWJZi~+sI Ɯ9~]Mhf7Ye0sD?/,ٶ-8NQ90P~cj473.:Z]vC{^AxCOl# &y>P#GG`W `g p1lOR)~Z1IpBJp77ٲQSS:f_ :iZV1D<>pV7'b/ [WGw(.)Pf$lT7ޠ{&U@ 4/({R|pcͣa$ 7S=޽E\}c&.i)!Fg RHM Te1q@\H=}&#ՀyND{Hɨw:5ﯫAY&YTQRYO$qbuqX"*$3'-Y~N.f [b㗰g$'oyT`=2ǝ8u M<ys߼8.~'mZqVE,A&*jЎKt6Hz(w s6TC|Bɺs7XiCjUߏ+U˜|락whTH26@!<`U%;beagW{7ioVo'Gϖ Ct\ к [q{-Rd=1ʽf'cMB7G_GAi1-yS)@OpM77_ }FC>Mj^x;G.qit'(OFWA $p7McPuHيj]DHA,hfyfg;ٞ}Ε߆/YN xP+-Iޗ *5ƧC63ԯx#pE3.wv:%u*ɐdo*tO:?~MdH4-VbR Kn +m_mtj~[&p+*Z!/+rD ecp4X"ΔQmpR*UUVW UpӅ9BjKb#N(fƛEpf$mO | 4&eh+>@ /) 8'YE,YZ}04jM2d&2@9v&Yw$E*O0\YmlS`K}03""h= 9VpD0NvuJQ4Jlcۛgدz3k'pEյg__LS?S?}{'"pS-|q@'RIM Ka^ƫor롽X48(|P+Oż0-˜`ѕaj &0cgjSwS^G-4NP(O}7X,~ZsǞtrf ̚+M`B[gӆF<~XP/EIv j"4-B6q'ûSgImF]*`8ȣOH;I>$t}P蒹alKTkI54C]ZHA{ M+Kw)g6E)Jv2@2&UCR.!llC.Pʯ:j\J2^xLog gߗ2o 0FWQ.>6fpv~ t/e5ygȟA2ntwy<0'oAϞEnB.h"؈gon!Sԯ*} SM;?<'_C'tTE9ݥc2 ,malأͯ^Yt~|=vUzPG1Bx%WCX&Yf#k{g VajX˧1W&A@MgIՓet9 1{#^i{F=`CF>#>Z˫gv)yHQfu8̡4|L^3.vd,L"V.'qu\)g;o_/q\8idq[)}V ȈQLkVQe#"qO|fA 4g_'j:aɡE Bi~E:}x(:^ѴKF|ѾĢ}j..Ni۽!ҁI?Rck f[osLЃ>QF 4IN""C>.5(F@|pqVU\+Z)d1fakemachine-0.0.9/testdata/test.gz000066400000000000000000000400341454747341100171200ustar00rootroot00000000000000batest@ ,qҩqdTfDj[ Kg5 "lP?BKk.Fv(vX =}',_>2]eI߇CS{I;\~.7}}YW |_& iYDoEƀum'Ee'lӈJ81j1.+yUnhF0'C ?3as/XDM+!x:{b}rG2H|[G R5 վ:fڭ01Z̳DpX5+NtDcC^,eSKXxLޟYի{A[? G\rr&"C;qekDZeRK8Q:uM3EkN,v ~* r\6sl>궈Z/aQ34)oثPRy ٦q/Hvi1 ^A)Md{654דּ巈b)4>%amj 1 |rzcy_${TBow ^{oPj/`,?.~$~c䝈x}|Рf4)W<߹u);BV-_Uʈ$\m.ʧ[PPK pa oO0K,I3ɭ9`g"rNdtiU(锃̔Re"dBR!`Bl Y;`0=K;N[kj§^r|wfiE? jDҩ6djTO`L^K:HL\<5˚r;#H@\wf|Wm˙ˢXu=cW~V5]y I7$N#@مyO㾹Z+}4as3VDX{8 j"<Eo(1>I$QZ+?ȿ[2>\gmoBeqa beFp2߁[za{g,}do-O01HtGk%վ?߁dbVգVaSWT^vK& iZs޽;}gnSE[;QIQ.PwRےw2ncU7Jw+/S|JU7yrF H:`9RfL9R15y?1#/3+ "cp3yqQV'<[ sl`-"̧~g[PjFs`,ÆyV4祉ݭGN;$Ö x{bWvkÙ032b|wۏB,d&L ta/{gZߦH Nu/_U6}Z?*NMiE~oo9%I$W__uTMwo|t ,H@\ӂ*fL% Y&ԂC`_&[okxDSѩkq\[v^mn*b  H3^Ia2M9Qnf8KXR,q:uOA^ E\j✄/ 0~M&_cZ +K1~jsDis@4QW.(Zv݀̀k]* L:) uO.eG;R;ޏߵyN?ޙ$S`R6vzNLGWWćN.Jbq' ij%{NZg@&#Yf FERƛ81bssMؐ~Z"&М<pu,X):`PliB?  uf"P$p'Z}lFOZ\ڣGQ2u![?#KݗB`*66zGI!Go0; zDl&jY&֏:upXl71|>ˤg`ΩBx;o7ӭ΢cltPN##~>E'ś28FRp-=$(z㬰gOga;]Kq̤Cx3 xc*ɦJd ͤirqjK)&wI3r7kdmn;GP$N EP+5T t-bS ~RugD :^,FπMha@uoZ樺cd3oAқʧa%c!cpW/<h!|b 0{YƼoW^ыz<I"Jy@{q ` mz>FUʲ}aF$vu98D9t*ghgIC6иz؋nqڞ> _3 .߁}xTv4gIa%җ Z~[<euPE6>sR*;B/OrƗi:c+M"N` }b ]%ͼ26B`lw#{V`#?t#C%?:BfN唖'ym xqW#>Q]g\ KH冶& ΥFkeCxӀΨ6EKwB!S񄕜9Rjܙ'.KSMC?ҷm3[W.VLFz]{\ ٯ߷N k+Ǥ8O>z.>Ǐ 8!BikZzTHS:cʻQqU#dd0\3Rd1O2ӄGMSTDƧ5K~UR(.вH~Fӱ}d$[3ZE$!;?mk=w{޽>|lIxzQbDɜq:HXUZCw UTU(VE߻ns08nCwvnu`aU#߻Y8Q&Ӫ] >V:Wd,u@]AOj@)F Ҁ`Yj&¦tJ OVA2``zpRZH`OF|O3)89OZӀl)qOCy|`w`ajF Xl,̚]vSy7i*wYЃ~IOF:y?ڡ[zU>׷R6q!R}9q+q-ޓۈwJ %N<ʗd680 t:EP-02GM׀"o[HtЄwr= L]zz4?Jel(B?tPWA(.D Xbww=m<`Ncm.^)Pbsdc6}3"PCgʑccl’˺y;+1 K֑,1' Ti>lE=t p:̸n|+Ӱv;qw j`qrFRn(BO~n brfw6_$.xGW7ۤq@'jAk-ig.uUL%(.-\nǚz >K1]̏ǘB~ikD@BU@U.uu[Ec % oP* EJa=/(^KGPBLGk"m~GO@:9傧KB 4:'4mR&+%?RR3vC޲@7:dF>W[?2|-6CfWV^W2F]ze 5Lj &xKLEs쩪1a6@b"mX2.$eJFClk]{ǘζ@+Dil!:Wy|`*?s#367mĔ0OSL⯞S,JЌe8iMA.yyBg:$EqmbQP! *K"_j; ҂81/*,#b@#;Ҡ:|2Sxoi@q7g4wϭ$SUuv!B&q2|  L9%t^d4`|MSYv-'ٖ͝9nÛ\o#ˠ 9H "# .NN;|>YػMqBP~4Ri}1fEt *tdEN T(t.>;ZxU qdM22wAXN{+uU'?2DJ=2uNH5FiE~D-oU Gmκ(7.aJNfqױC TB0D&"2@Ȭ+u QvB Tbp*(x2- k ӁalnUw-!Ir*#wLbG\- gv330l @VqWpDo4eI]ˬ CMoHl\X <7>xH{a7}(! aT1O| mhlzYau?b& WfͶ{xe4Ri/Fs`0_RM47K;2bQiH9:| R4V|$|VP̫(ؕؒ |.98H11nF [l qy ӁަlZ]K`NwĉqO) j>DE} &nUGRCtȗX$Dq> Of&x=w4SdQOǵwLb2q)˧ bv1ȏ2r=D{?kKe)n(IjM)|ږ*bL1==\ !*=5͛#)Za%8 QN7+>m:Z9a2&mK!'ぇR9~iz[MR~r^ϸe'y!Ra,FY1lwҐ>xsR]6\XhUþg3yMxͷ+ fxp圩eC"X y}3bVwi,@@[A[W:q1˭5,/v!TcÊ9bT~_aL|6ME: FK9MdRL3 {1prwt€S͉^G0~UI=mȍv^BN{&t);޻٩+2o7(y@1p߸6wg EHM535,Q~2p?,zwzhq408d=`,t`6H=7$+MSlKS*;4Cە8/ԙhvg|?n5QVB?,ƥlx=SfqhS;g}*Vփg@ JcS6o gEnj ”)]fIQGeޮlˆu$_09]o.eQN>A:#GL;$DȻ;sml#>'+ =[SvVen٠sMzMr4"OQln#wk%47lj'W޷Vkp<=WYK*#@'d )"T#Sgup. '*[6Vt% "ޖn$z=#.! f2&'eT uY =Ӂ! HW &O&=,*|\,e A)-oEKC%grG(dpy Ru qU^ `͘}ޔdVT`z}WFJB[Mo:{qAhҖҴ9h$G]jFŖ> &oGb4{yӼ>􅙩ѩ}6{ZC a[m(x1M3M(/B+*=S)o;jH[F5as}]u4ե,Э]tc^!B #7X QʯiQG9`Pf)$Ї(ZfJ!c5RSi&U:6oL%L|HHgf%l~+7dfM'6E$#e׷߃PfW/.4"z:&d)Ewl.@sۢffq,N^;v' -J fdڰ_}PQ!|Y7;n"joyO.^bs@x/] m-в0,5,wwnot8/Y EdmtOM TP=;ɍWJZi~+sI Ɯ9~]Mhf7Ye0sD?/,ٶ-8NQ90P~cj473.:Z]vC{^AxCOl# &y>P#GG`W `g p1lOR)~Z1IpBJp77ٲQSS:f_ :iZV1D<>pV7'b/ [WGw(.)Pf$lT7ޠ{&U@ 4/({R|pcͣa$ 7S=޽E\}c&.i)!Fg RHM Te1q@\H=}&#ՀyND{Hɨw:5ﯫAY&YTQRYO$qbuqX"*$3'-Y~N.f [b㗰g$'oyT`=2ǝ8u M<ys߼8.~'mZqVE,A&*jЎKt6Hz(w s6TC|Bɺs7XiCjUߏ+U˜|락whTH26@!<`U%;beagW{7ioVo'Gϖ Ct\ к [q{-Rd=1ʽf'cMB7G_GAi1-yS)@OpM77_ }FC>Mj^x;G.qit'(OFWA $p7McPuHيj]DHA,hfyfg;ٞ}Ε߆/YN xP+-Iޗ *5ƧC63ԯx#pE3.wv:%u*ɐdo*tO:?~MdH4-VbR Kn +m_mtj~[&p+*Z!/+rD ecp4X"ΔQmpR*UUVW UpӅ9BjKb#N(fƛEpf$mO | 4&eh+>@ /) 8'YE,YZ}04jM2d&2@9v&Yw$E*O0\YmlS`K}03""h= 9VpD0NvuJQ4Jlcۛgدz3k'pEյg__LS?S?}{'"pS-|q@'RIM Ka^ƫor롽X48(|P+Oż0-˜`ѕaj &0cgjSwS^G-4NP(O}7X,~ZsǞtrf ̚+M`B[gӆF<~XP/EIv j"4-B6q'ûSgImF]*`8ȣOH;I>$t}P蒹alKTkI54C]ZHA{ M+Kw)g6E)Jv2@2&UCR.!llC.Pʯ:j\J2^xLog gߗ2o 0FWQ.>6fpv~ t/e5ygȟA2ntwy<0'oAϞEnB.h"؈gon!Sԯ*} SM;?<'_C'tTE9ݥc2 ,malأͯ^Yt~|=vUzPG1Bx%WCX&Yf#k{g VajX˧1W&A@MgIՓet9 1{#^i{F=`CF>#>Z˫gv)yHQfu8̡4|L^3.vd,L"V.'qu\)g;o_/q\8idq[)}V ȈQLkVQe#"qO|fA 4g_'j:aɡE Bi~E:}x(:^ѴKF|ѾĢ}j..Ni۽!ҁI?Rck f[osLЃ>QF 4IN""C>.5(F@|pqVU\+Z)d1o@fakemachine-0.0.9/testdata/test.xz000066400000000000000000000400741454747341100171450ustar00rootroot000000000000007zXZִF!X? ,qҩqdTfDj[ Kg5 "lP?BKk.Fv(vX =}',_>2]eI߇CS{I;\~.7}}YW |_& iYDoEƀum'Ee'lӈJ81j1.+yUnhF0'C ?3as/XDM+!x:{b}rG2H|[G R5 վ:fڭ01Z̳DpX5+NtDcC^,eSKXxLޟYի{A[? G\rr&"C;qekDZeRK8Q:uM3EkN,v ~* r\6sl>궈Z/aQ34)oثPRy ٦q/Hvi1 ^A)Md{654דּ巈b)4>%amj 1 |rzcy_${TBow ^{oPj/`,?.~$~c䝈x}|Рf4)W<߹u);BV-_Uʈ$\m.ʧ[PPK pa oO0K,I3ɭ9`g"rNdtiU(锃̔Re"dBR!`Bl Y;`0=K;N[kj§^r|wfiE? jDҩ6djTO`L^K:HL\<5˚r;#H@\wf|Wm˙ˢXu=cW~V5]y I7$N#@مyO㾹Z+}4as3VDX{8 j"<Eo(1>I$QZ+?ȿ[2>\gmoBeqa beFp2߁[za{g,}do-O01HtGk%վ?߁dbVգVaSWT^vK& iZs޽;}gnSE[;QIQ.PwRےw2ncU7Jw+/S|JU7yrF H:`9RfL9R15y?1#/3+ "cp3yqQV'<[ sl`-"̧~g[PjFs`,ÆyV4祉ݭGN;$Ö x{bWvkÙ032b|wۏB,d&L ta/{gZߦH Nu/_U6}Z?*NMiE~oo9%I$W__uTMwo|t ,H@\ӂ*fL% Y&ԂC`_&[okxDSѩkq\[v^mn*b  H3^Ia2M9Qnf8KXR,q:uOA^ E\j✄/ 0~M&_cZ +K1~jsDis@4QW.(Zv݀̀k]* L:) uO.eG;R;ޏߵyN?ޙ$S`R6vzNLGWWćN.Jbq' ij%{NZg@&#Yf FERƛ81bssMؐ~Z"&М<pu,X):`PliB?  uf"P$p'Z}lFOZ\ڣGQ2u![?#KݗB`*66zGI!Go0; zDl&jY&֏:upXl71|>ˤg`ΩBx;o7ӭ΢cltPN##~>E'ś28FRp-=$(z㬰gOga;]Kq̤Cx3 xc*ɦJd ͤirqjK)&wI3r7kdmn;GP$N EP+5T t-bS ~RugD :^,FπMha@uoZ樺cd3oAқʧa%c!cpW/<h!|b 0{YƼoW^ыz<I"Jy@{q ` mz>FUʲ}aF$vu98D9t*ghgIC6иz؋nqڞ> _3 .߁}xTv4gIa%җ Z~[<euPE6>sR*;B/OrƗi:c+M"N` }b ]%ͼ26B`lw#{V`#?t#C%?:BfN唖'ym xqW#>Q]g\ KH冶& ΥFkeCxӀΨ6EKwB!S񄕜9Rjܙ'.KSMC?ҷm3[W.VLFz]{\ ٯ߷N k+Ǥ8O>z.>Ǐ 8!BikZzTHS:cʻQqU#dd0\3Rd1O2ӄGMSTDƧ5K~UR(.вH~Fӱ}d$[3ZE$!;?mk=w{޽>|lIxzQbDɜq:HXUZCw UTU(VE߻ns08nCwvnu`aU#߻Y8Q&Ӫ] >V:Wd,u@]AOj@)F Ҁ`Yj&¦tJ OVA2``zpRZH`OF|O3)89OZӀl)qOCy|`w`ajF Xl,̚]vSy7i*wYЃ~IOF:y?ڡ[zU>׷R6q!R}9q+q-ޓۈwJ %N<ʗd680 t:EP-02GM׀"o[HtЄwr= L]zz4?Jel(B?tPWA(.D Xbww=m<`Ncm.^)Pbsdc6}3"PCgʑccl’˺y;+1 K֑,1' Ti>lE=t p:̸n|+Ӱv;qw j`qrFRn(BO~n brfw6_$.xGW7ۤq@'jAk-ig.uUL%(.-\nǚz >K1]̏ǘB~ikD@BU@U.uu[Ec % oP* EJa=/(^KGPBLGk"m~GO@:9傧KB 4:'4mR&+%?RR3vC޲@7:dF>W[?2|-6CfWV^W2F]ze 5Lj &xKLEs쩪1a6@b"mX2.$eJFClk]{ǘζ@+Dil!:Wy|`*?s#367mĔ0OSL⯞S,JЌe8iMA.yyBg:$EqmbQP! *K"_j; ҂81/*,#b@#;Ҡ:|2Sxoi@q7g4wϭ$SUuv!B&q2|  L9%t^d4`|MSYv-'ٖ͝9nÛ\o#ˠ 9H "# .NN;|>YػMqBP~4Ri}1fEt *tdEN T(t.>;ZxU qdM22wAXN{+uU'?2DJ=2uNH5FiE~D-oU Gmκ(7.aJNfqױC TB0D&"2@Ȭ+u QvB Tbp*(x2- k ӁalnUw-!Ir*#wLbG\- gv330l @VqWpDo4eI]ˬ CMoHl\X <7>xH{a7}(! aT1O| mhlzYau?b& WfͶ{xe4Ri/Fs`0_RM47K;2bQiH9:| R4V|$|VP̫(ؕؒ |.98H11nF [l qy ӁަlZ]K`NwĉqO) j>DE} &nUGRCtȗX$Dq> Of&x=w4SdQOǵwLb2q)˧ bv1ȏ2r=D{?kKe)n(IjM)|ږ*bL1==\ !*=5͛#)Za%8 QN7+>m:Z9a2&mK!'ぇR9~iz[MR~r^ϸe'y!Ra,FY1lwҐ>xsR]6\XhUþg3yMxͷ+ fxp圩eC"X y}3bVwi,@@[A[W:q1˭5,/v!TcÊ9bT~_aL|6ME: FK9MdRL3 {1prwt€S͉^G0~UI=mȍv^BN{&t);޻٩+2o7(y@1p߸6wg EHM535,Q~2p?,zwzhq408d=`,t`6H=7$+MSlKS*;4Cە8/ԙhvg|?n5QVB?,ƥlx=SfqhS;g}*Vփg@ JcS6o gEnj ”)]fIQGeޮlˆu$_09]o.eQN>A:#GL;$DȻ;sml#>'+ =[SvVen٠sMzMr4"OQln#wk%47lj'W޷Vkp<=WYK*#@'d )"T#Sgup. '*[6Vt% "ޖn$z=#.! f2&'eT uY =Ӂ! HW &O&=,*|\,e A)-oEKC%grG(dpy Ru qU^ `͘}ޔdVT`z}WFJB[Mo:{qAhҖҴ9h$G]jFŖ> &oGb4{yӼ>􅙩ѩ}6{ZC a[m(x1M3M(/B+*=S)o;jH[F5as}]u4ե,Э]tc^!B #7X QʯiQG9`Pf)$Ї(ZfJ!c5RSi&U:6oL%L|HHgf%l~+7dfM'6E$#e׷߃PfW/.4"z:&d)Ewl.@sۢffq,N^;v' -J fdڰ_}PQ!|Y7;n"joyO.^bs@x/] m-в0,5,wwnot8/Y EdmtOM TP=;ɍWJZi~+sI Ɯ9~]Mhf7Ye0sD?/,ٶ-8NQ90P~cj473.:Z]vC{^AxCOl# &y>P#GG`W `g p1lOR)~Z1IpBJp77ٲQSS:f_ :iZV1D<>pV7'b/ [WGw(.)Pf$lT7ޠ{&U@ 4/({R|pcͣa$ 7S=޽E\}c&.i)!Fg RHM Te1q@\H=}&#ՀyND{Hɨw:5ﯫAY&YTQRYO$qbuqX"*$3'-Y~N.f [b㗰g$'oyT`=2ǝ8u M<ys߼8.~'mZqVE,A&*jЎKt6Hz(w s6TC|Bɺs7XiCjUߏ+U˜|락whTH26@!<`U%;beagW{7ioVo'Gϖ Ct\ к [q{-Rd=1ʽf'cMB7G_GAi1-yS)@OpM77_ }FC>Mj^x;G.qit'(OFWA $p7McPuHيj]DHA,hfyfg;ٞ}Ε߆/YN xP+-Iޗ *5ƧC63ԯx#pE3.wv:%u*ɐdo*tO:?~MdH4-VbR Kn +m_mtj~[&p+*Z!/+rD ecp4X"ΔQmpR*UUVW UpӅ9BjKb#N(fƛEpf$mO | 4&eh+>@ /) 8'YE,YZ}04jM2d&2@9v&Yw$E*O0\YmlS`K}03""h= 9VpD0NvuJQ4Jlcۛgدz3k'pEյg__LS?S?}{'"pS-|q@'RIM Ka^ƫor롽X48(|P+Oż0-˜`ѕaj &0cgjSwS^G-4NP(O}7X,~ZsǞtrf ̚+M`B[gӆF<~XP/EIv j"4-B6q'ûSgImF]*`8ȣOH;I>$t}P蒹alKTkI54C]ZHA{ M+Kw)g6E)Jv2@2&UCR.!llC.Pʯ:j\J2^xLog gߗ2o 0FWQ.>6fpv~ t/e5ygȟA2ntwy<0'oAϞEnB.h"؈gon!Sԯ*} SM;?<'_C'tTE9ݥc2 ,malأͯ^Yt~|=vUzPG1Bx%WCX&Yf#k{g VajX˧1W&A@MgIՓet9 1{#^i{F=`CF>#>Z˫gv)yHQfu8̡4|L^3.vd,L"V.'qu\)g;o_/q\8idq[)}V ȈQLkVQe#"qO|fA 4g_'j:aɡE Bi~E:}x(:^ѴKF|ѾĢ}j..Ni۽!ҁI?Rck f[osLЃ>QF 4IN""C>.5(F@|pqVU\+Z)d1nZL*Y$gYZfakemachine-0.0.9/testdata/test.zst000066400000000000000000000400161454747341100173200ustar00rootroot00000000000000(/d? ,qҩqdTfDj[ Kg5 "lP?BKk.Fv(vX =}',_>2]eI߇CS{I;\~.7}}YW |_& iYDoEƀum'Ee'lӈJ81j1.+yUnhF0'C ?3as/XDM+!x:{b}rG2H|[G R5 վ:fڭ01Z̳DpX5+NtDcC^,eSKXxLޟYի{A[? G\rr&"C;qekDZeRK8Q:uM3EkN,v ~* r\6sl>궈Z/aQ34)oثPRy ٦q/Hvi1 ^A)Md{654דּ巈b)4>%amj 1 |rzcy_${TBow ^{oPj/`,?.~$~c䝈x}|Рf4)W<߹u);BV-_Uʈ$\m.ʧ[PPK pa oO0K,I3ɭ9`g"rNdtiU(锃̔Re"dBR!`Bl Y;`0=K;N[kj§^r|wfiE? jDҩ6djTO`L^K:HL\<5˚r;#H@\wf|Wm˙ˢXu=cW~V5]y I7$N#@مyO㾹Z+}4as3VDX{8 j"<Eo(1>I$QZ+?ȿ[2>\gmoBeqa beFp2߁[za{g,}do-O01HtGk%վ?߁dbVգVaSWT^vK& iZs޽;}gnSE[;QIQ.PwRےw2ncU7Jw+/S|JU7yrF H:`9RfL9R15y?1#/3+ "cp3yqQV'<[ sl`-"̧~g[PjFs`,ÆyV4祉ݭGN;$Ö x{bWvkÙ032b|wۏB,d&L ta/{gZߦH Nu/_U6}Z?*NMiE~oo9%I$W__uTMwo|t ,H@\ӂ*fL% Y&ԂC`_&[okxDSѩkq\[v^mn*b  H3^Ia2M9Qnf8KXR,q:uOA^ E\j✄/ 0~M&_cZ +K1~jsDis@4QW.(Zv݀̀k]* L:) uO.eG;R;ޏߵyN?ޙ$S`R6vzNLGWWćN.Jbq' ij%{NZg@&#Yf FERƛ81bssMؐ~Z"&М<pu,X):`PliB?  uf"P$p'Z}lFOZ\ڣGQ2u![?#KݗB`*66zGI!Go0; zDl&jY&֏:upXl71|>ˤg`ΩBx;o7ӭ΢cltPN##~>E'ś28FRp-=$(z㬰gOga;]Kq̤Cx3 xc*ɦJd ͤirqjK)&wI3r7kdmn;GP$N EP+5T t-bS ~RugD :^,FπMha@uoZ樺cd3oAқʧa%c!cpW/<h!|b 0{YƼoW^ыz<I"Jy@{q ` mz>FUʲ}aF$vu98D9t*ghgIC6иz؋nqڞ> _3 .߁}xTv4gIa%җ Z~[<euPE6>sR*;B/OrƗi:c+M"N` }b ]%ͼ26B`lw#{V`#?t#C%?:BfN唖'ym xqW#>Q]g\ KH冶& ΥFkeCxӀΨ6EKwB!S񄕜9Rjܙ'.KSMC?ҷm3[W.VLFz]{\ ٯ߷N k+Ǥ8O>z.>Ǐ 8!BikZzTHS:cʻQqU#dd0\3Rd1O2ӄGMSTDƧ5K~UR(.вH~Fӱ}d$[3ZE$!;?mk=w{޽>|lIxzQbDɜq:HXUZCw UTU(VE߻ns08nCwvnu`aU#߻Y8Q&Ӫ] >V:Wd,u@]AOj@)F Ҁ`Yj&¦tJ OVA2``zpRZH`OF|O3)89OZӀl)qOCy|`w`ajF Xl,̚]vSy7i*wYЃ~IOF:y?ڡ[zU>׷R6q!R}9q+q-ޓۈwJ %N<ʗd680 t:EP-02GM׀"o[HtЄwr= L]zz4?Jel(B?tPWA(.D Xbww=m<`Ncm.^)Pbsdc6}3"PCgʑccl’˺y;+1 K֑,1' Ti>lE=t p:̸n|+Ӱv;qw j`qrFRn(BO~n brfw6_$.xGW7ۤq@'jAk-ig.uUL%(.-\nǚz >K1]̏ǘB~ikD@BU@U.uu[Ec % oP* EJa=/(^KGPBLGk"m~GO@:9傧KB 4:'4mR&+%?RR3vC޲@7:dF>W[?2|-6CfWV^W2F]ze 5Lj &xKLEs쩪1a6@b"mX2.$eJFClk]{ǘζ@+Dil!:Wy|`*?s#367mĔ0OSL⯞S,JЌe8iMA.yyBg:$EqmbQP! *K"_j; ҂81/*,#b@#;Ҡ:|2Sxoi@q7g4wϭ$SUuv!B&q2|  L9%t^d4`|MSYv-'ٖ͝9nÛ\o#ˠ 9H "# .NN;|>YػMqBP~4Ri}1fEt *tdEN T(t.>;ZxU qdM22wAXN{+uU'?2DJ=2uNH5FiE~D-oU Gmκ(7.aJNfqױC TB0D&"2@Ȭ+u QvB Tbp*(x2- k ӁalnUw-!Ir*#wLbG\- gv330l @VqWpDo4eI]ˬ CMoHl\X <7>xH{a7}(! aT1O| mhlzYau?b& WfͶ{xe4Ri/Fs`0_RM47K;2bQiH9:| R4V|$|VP̫(ؕؒ |.98H11nF [l qy ӁަlZ]K`NwĉqO) j>DE} &nUGRCtȗX$Dq> Of&x=w4SdQOǵwLb2q)˧ bv1ȏ2r=D{?kKe)n(IjM)|ږ*bL1==\ !*=5͛#)Za%8 QN7+>m:Z9a2&mK!'ぇR9~iz[MR~r^ϸe'y!Ra,FY1lwҐ>xsR]6\XhUþg3yMxͷ+ fxp圩eC"X y}3bVwi,@@[A[W:q1˭5,/v!TcÊ9bT~_aL|6ME: FK9MdRL3 {1prwt€S͉^G0~UI=mȍv^BN{&t);޻٩+2o7(y@1p߸6wg EHM535,Q~2p?,zwzhq408d=`,t`6H=7$+MSlKS*;4Cە8/ԙhvg|?n5QVB?,ƥlx=SfqhS;g}*Vփg@ JcS6o gEnj ”)]fIQGeޮlˆu$_09]o.eQN>A:#GL;$DȻ;sml#>'+ =[SvVen٠sMzMr4"OQln#wk%47lj'W޷Vkp<=WYK*#@'d )"T#Sgup. '*[6Vt% "ޖn$z=#.! f2&'eT uY =Ӂ! HW &O&=,*|\,e A)-oEKC%grG(dpy Ru qU^ `͘}ޔdVT`z}WFJB[Mo:{qAhҖҴ9h$G]jFŖ> &oGb4{yӼ>􅙩ѩ}6{ZC a[m(x1M3M(/B+*=S)o;jH[F5as}]u4ե,Э]tc^!B #7X QʯiQG9`Pf)$Ї(ZfJ!c5RSi&U:6oL%L|HHgf%l~+7dfM'6E$#e׷߃PfW/.4"z:&d)Ewl.@sۢffq,N^;v' -J fdڰ_}PQ!|Y7;n"joyO.^bs@x/] m-в0,5,wwnot8/Y EdmtOM TP=;ɍWJZi~+sI Ɯ9~]Mhf7Ye0sD?/,ٶ-8NQ90P~cj473.:Z]vC{^AxCOl# &y>P#GG`W `g p1lOR)~Z1IpBJp77ٲQSS:f_ :iZV1D<>pV7'b/ [WGw(.)Pf$lT7ޠ{&U@ 4/({R|pcͣa$ 7S=޽E\}c&.i)!Fg RHM Te1q@\H=}&#ՀyND{Hɨw:5ﯫAY&YTQRYO$qbuqX"*$3'-Y~N.f [b㗰g$'oyT`=2ǝ8u M<ys߼8.~'mZqVE,A&*jЎKt6Hz(w s6TC|Bɺs7XiCjUߏ+U˜|락whTH26@!<`U%;beagW{7ioVo'Gϖ Ct\ к [q{-Rd=1ʽf'cMB7G_GAi1-yS)@OpM77_ }FC>Mj^x;G.qit'(OFWA $p7McPuHيj]DHA,hfyfg;ٞ}Ε߆/YN xP+-Iޗ *5ƧC63ԯx#pE3.wv:%u*ɐdo*tO:?~MdH4-VbR Kn +m_mtj~[&p+*Z!/+rD ecp4X"ΔQmpR*UUVW UpӅ9BjKb#N(fƛEpf$mO | 4&eh+>@ /) 8'YE,YZ}04jM2d&2@9v&Yw$E*O0\YmlS`K}03""h= 9VpD0NvuJQ4Jlcۛgدz3k'pEյg__LS?S?}{'"pS-|q@'RIM Ka^ƫor롽X48(|P+Oż0-˜`ѕaj &0cgjSwS^G-4NP(O}7X,~ZsǞtrf ̚+M`B[gӆF<~XP/EIv j"4-B6q'ûSgImF]*`8ȣOH;I>$t}P蒹alKTkI54C]ZHA{ M+Kw)g6E)Jv2@2&UCR.!llC.Pʯ:j\J2^xLog gߗ2o 0FWQ.>6fpv~ t/e5ygȟA2ntwy<0'oAϞEnB.h"؈gon!Sԯ*} SM;?<'_C'tTE9ݥc2 ,malأͯ^Yt~|=vUzPG1Bx%WCX&Yf#k{g VajX˧1W&A@MgIՓet9 1{#^i{F=`CF>#>Z˫gv)yHQfu8̡4|L^3.vd,L"V.'qu\)g;o_/q\8idq[)}V ȈQLkVQe#"qO|fA 4g_'j:aɡE Bi~E:}x(:^ѴKF|ѾĢ}j..Ni۽!ҁI?Rck f[osLЃ>QF 4IN""C>.5(F@|pqVU\+Z)d1,[