pax_global_header00006660000000000000000000000064150057232310014510gustar00rootroot0000000000000052 comment=bad8d9fb2e96ddfa4f2f33ab6c8a508d7d76ad12 virtme-ng-1.35/000077500000000000000000000000001500572323100133505ustar00rootroot00000000000000virtme-ng-1.35/.editorconfig000066400000000000000000000013011500572323100160200ustar00rootroot00000000000000# Check https://editorconfig.org for details root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space tab_width = 4 trim_trailing_whitespace = true [*.sh] shell_variant = bash # used by `shfmt` indent_style = space indent_size = 4 # Supported by `shfmt`, but not official EditorConfig specification. See # https://github.com/mvdan/sh/commit/7f96e7d84a265f4d1005b96493422cde800bf9d1 # for details. [[shell]] indent_style = space indent_size = 4 space_redirects = true [*.y{a,}ml] indent_style = space indent_size = 2 [*.py] indent_style = space indent_size = 4 [{Makefile,*.mak}] indent_style = tab [{COMMIT_EDITMSG,EDIT_DESCRIPTION}] max_line_length = 72 virtme-ng-1.35/.git-blame-ignore-revs000066400000000000000000000002271500572323100174510ustar00rootroot00000000000000# Ignore code formatting commit d0d90eb3d1b1558ac29d3338f26b353c831bfffe # Ignore bash code formatting commit 636713c7eee8d309c179617601a466b88ab918c2 virtme-ng-1.35/.github/000077500000000000000000000000001500572323100147105ustar00rootroot00000000000000virtme-ng-1.35/.github/workflows/000077500000000000000000000000001500572323100167455ustar00rootroot00000000000000virtme-ng-1.35/.github/workflows/pylint.yml000066400000000000000000000015551500572323100210150ustar00rootroot00000000000000name: Pylint on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | set -euxo pipefail python -m pip install --upgrade pip # Both build and runtime deps. This is the price of using pip. pip install 'argparse-manpage[setuptools]' argcomplete requests pip install pylint flake8 - name: Analysing the code with pylint run: | set -euxo pipefail pylint vng '**/*.py' flake8 vng find . -name '*.py' | xargs flake8 virtme-ng-1.35/.github/workflows/run.yml000066400000000000000000000023021500572323100202710ustar00rootroot00000000000000name: Run on: push: branches: - main pull_request: jobs: run: runs-on: ubuntu-22.04 steps: ### DEPENDENCIES ### # Hard turn-off interactive mode - run: echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections # Install dependencies - run: sudo apt update - run: sudo apt install --yes git qemu-kvm udev iproute2 busybox-static coreutils python3-requests libvirt-clients kbd kmod file rsync zstd udev ### END DEPENDENCIES ### # Checkout git repository - uses: actions/checkout@v4 # Run `uname -r` using a vanilla v6.6 kernel - run: ./vng -r v6.6 -- uname -r # Setup KVM support - name: "KVM support" run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm # Re-run with KVM support - name: "Check KVM support" run: | clocksource="/sys/devices/system/clocksource/clocksource0/current_clocksource" [ "$(./vng -r v6.6 -- cat "${clocksource}")" = "kvm-clock" ] virtme-ng-1.35/.github/workflows/rust.yml000066400000000000000000000006441500572323100204710ustar00rootroot00000000000000name: Rust on: push: branches: - main pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest defaults: run: working-directory: ./virtme_ng_init steps: - uses: actions/checkout@v3 - name: Coding style run: cargo fmt -- --check - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose virtme-ng-1.35/.gitignore000066400000000000000000000005601500572323100153410ustar00rootroot00000000000000*~ __pycache__ build dist .mypy_cache virtme-ng-prompt vng-prompt vng.1 virtme_ng.egg-info/ .pybuild/ debian/virtme-ng/ debian/.debhelper/ debian/debhelper-build-stamp debian/files debian/virtme-ng.debhelper.log debian/virtme-ng.postinst.debhelper debian/virtme-ng.prerm.debhelper debian/virtme-ng.substvars virtme_ng_init/target virtme/guest/.crate* virtme/guest/bin virtme-ng-1.35/.gitmodules000066400000000000000000000000001500572323100155130ustar00rootroot00000000000000virtme-ng-1.35/.pre-commit-config.yaml000066400000000000000000000012751500572323100176360ustar00rootroot00000000000000--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: end-of-file-fixer exclude: 'DCO-1\.1\.txt' - id: trailing-whitespace - id: check-merge-conflict - id: mixed-line-ending - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.7 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.10.0.1 hooks: - id: shellcheck - repo: https://github.com/scop/pre-commit-shfmt rev: v3.11.0-1 hooks: - id: shfmt virtme-ng-1.35/DCO-1.1.txt000066400000000000000000000023741500572323100150210ustar00rootroot00000000000000The text 'Signed-off-by:' in a commit message indicates that the signer agrees to the Developer's Certificate of Origin 1.1, reproduced below. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. virtme-ng-1.35/LICENSE000066400000000000000000000432541500572323100143650ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. virtme-ng-1.35/Makefile000066400000000000000000000015001500572323100150040ustar00rootroot00000000000000INSTALL_ARGS := # todo: add --break-system-packages if ubuntu # Get git version information for make install GIT_DESCRIBE := $(shell git describe --always --long --dirty) .PHONY: all init clean install install_from_source install_only_top all: init virtme/guest/bin/virtme-ng-init: virtme_ng_init/src/*.rs BUILD_VIRTME_NG_INIT=1 python3 setup.py build init: virtme/guest/bin/virtme-ng-init @echo "Version: $(GIT_DESCRIBE)" clean: BUILD_VIRTME_NG_INIT=1 python3 setup.py clean rm -f virtme/guest/bin/virtme-ng-init # see README.md '* Install from source' install: install_from_source install_from_source: @echo "Version: $(GIT_DESCRIBE)" BUILD_VIRTME_NG_INIT=1 pip3 install --verbose $(INSTALL_ARGS) . install_only_top: @echo "Version: $(GIT_DESCRIBE)" BUILD_VIRTME_NG_INIT=0 pip3 install --verbose $(INSTALL_ARGS) . virtme-ng-1.35/README.md000066400000000000000000000532331500572323100146350ustar00rootroot00000000000000https://github.com/arighi/virtme-ng/assets/423281/485608ee-0c82-46d1-b311-e1b7af0a4e44 What is virtme-ng? ==================== virtme-ng is a tool that allows to easily and quickly recompile and test a Linux kernel, starting from the source code. It allows to recompile the kernel in few minutes (rather than hours), then the kernel is automatically started in a virtualized environment that is an exact copy-on-write copy of your live system, which means that any changes made to the virtualized environment do not affect the host system. In order to do this a minimal config is produced (with the bare minimum support to test the kernel inside qemu), then the selected kernel is automatically built and started inside qemu, using the filesystem of the host as a copy-on-write snapshot. This means that you can safely destroy the entire filesystem, crash the kernel, etc. without affecting the host. Kernels produced with virtme-ng are lacking lots of features, in order to reduce the build time to the minimum and still provide you a usable kernel capable of running your tests and experiments. virtme-ng is based on virtme, written by Andy Lutomirski ([web][korg-web] | [git][korg-git]). Quick start =========== ``` $ uname -r 5.19.0-23-generic $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git $ cd linux $ vng --build --commit v6.2-rc4 ... $ vng _ _ __ _(_)_ __| |_ _ __ ___ ___ _ __ __ _ \ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ | \ V /| | | | |_| | | | | | __/_____| | | | (_| | \_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ | |___/ kernel version: 6.2.0-rc4-virtme x86_64 $ uname -r 6.2.0-rc4-virtme ^ |___ Now you have a shell inside a virtualized copy of your entire system, that is running the new kernel! \o/ Then simply type "exit" to return back to the real system. ``` Installation ============ * Debian / Ubuntu You can install the latest stable version of virtme-ng via: ``` $ sudo apt install virtme-ng ``` * Ubuntu ppa If you're using Ubuntu, you can install the latest experimental version of virtme-ng from ppa:arighi/virtme-ng: ``` $ sudo add-apt-repository ppa:arighi/virtme-ng $ sudo apt install --yes virtme-ng ``` * Install from source To install virtme-ng from source you can clone this git repository and build a standalone virtme-ng running the following commands: ``` $ git clone --recurse-submodules https://github.com/arighi/virtme-ng.git $ BUILD_VIRTME_NG_INIT=1 pip3 install . ``` If you are in Debian/Ubuntu you may need to install the following packages to build virtme-ng from source properly: ``` $ sudo apt install python3-pip flake8 pylint cargo rustc qemu-system-x86 ``` If you'd prefer to use `uv`: ``` $ BUILD_VIRTME_NG_INIT=1 uv tool install . ``` * Run from source You can also run virtme-ng directly from source, make sure you have all the requirements installed (optionally you can build `virtme-ng-init` for a faster boot, by running `make`), then from the source directory simply run any virtme-ng command, such as: ``` $ ./vng --help ``` Requirements ============ * You need Python 3.8 or higher * QEMU 1.6 or higher is recommended (QEMU 1.4 and 1.5 are partially supported using a rather ugly kludge) * You will have a much better experience if KVM is enabled. That means that you should be on bare metal with hardware virtualization (VT-x or SVM) enabled or in a VM that supports nested virtualization. On some Linux distributions, you may need to be a member of the "kvm" group. Using VirtualBox or most VPS providers will fall back to emulation. If you are using GitHub Actions, KVM support is supported on "larger Linux runners" -- which is [now](https://github.blog/2024-01-17-github-hosted-runners-double-the-power-for-open-source/) the default runner -- but it has to be [manually enabled](https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/), see how it is used in [our tests](.github/workflows/run.yml) or [here](https://github.com/multipath-tcp/mptcp_net-next/commit/677b5ecd223c) with Docker. * Depending on the options you use, you may need a statically linked `busybox` binary somewhere in your path. * Optionally, you may need virtiofsd 1.7.0 (or higher) for better filesystem performance inside the virtme-ng guests. * Optionally, you may need `socat` for the `--console` and `--console-client` options, and the host's kernel should support VSOCK (`CONFIG_VHOST_VSOCK`). * Optionally, you may need `sshd` installed for the `--ssh` and `--ssh-client` options. Examples ======== - Build a kernel from a clean local kernel source directory (if a .config is not available virtme-ng will automatically create a minimum .config with all the required feature to boot the instance): ``` $ vng -b ``` - Build tag v6.1-rc3 from a local kernel git repository: ``` $ vng -b -c v6.1-rc3 ``` - Generate a minimal kernel .config in the current kernel build directory: ``` $ vng --kconfig ``` - Run a kernel previously compiled from a local git repository in the current working directory: ``` $ vng ``` - Run an interactive virtme-ng session using the same kernel as the host: ``` $ vng -r ``` - Test installed kernel 6.2.0-21-generic kernel (NOTE: /boot/vmlinuz-6.2.0-21-generic needs to be accessible): ``` $ vng -r 6.2.0-21-generic ``` - Run a pre-compiled vanilla v6.6 kernel fetched from the Ubuntu mainline builds repository (useful to test a specific kernel version directly and save a lot of build time): ``` $ vng -r v6.6 ``` - Download and test kernel 6.2.0-1003-lowlatency from deb packages: ``` $ mkdir test $ cd test $ apt download linux-image-6.2.0-1003-lowlatency linux-modules-6.2.0-1003-lowlatency $ for d in *.deb; do dpkg -x $d .; done $ vng -r ./boot/vmlinuz-6.2.0-1003-lowlatency ``` - Build the tip of the latest kernel on a remote build host called "builder", running make inside a specific build chroot (managed remotely by schroot): ``` $ vng --build --build-host builder \ --build-host-exec-prefix "schroot -c chroot:kinetic-amd64 -- " ``` - Run the previously compiled kernel from the current working directory and enable networking: ``` $ vng --net user ``` - Run the previously compiled kernel adding an additional virtio-scsi device: ``` $ qemu-img create -f qcow2 /tmp/disk.img 8G $ vng --disk /tmp/disk.img ``` - Recompile the kernel passing some env variables to enable Rust support (using specific versions of the Rust toolchain binaries): ``` $ vng --build RUSTC=rustc-1.62 BINDGEN=bindgen-0.56 RUSTFMT=rustfmt-1.62 ``` - Build the arm64 kernel (using a separate chroot in /opt/chroot/arm64 as the main filesystem): ``` $ vng --build --arch arm64 --root /opt/chroot/arm64/ ``` - Build the kernel using a separate build directory, and run it, in verbose: ``` $ export KBUILD_OUTPUT=.virtme/build $ vng --build --verbose $ vng --verbose ``` - Same example, but using `O=`: ``` $ vng --build --verbose -- O=.virtme/build $ vng --verbose -- O=.virtme/build ``` - Accelerate the kernel rebuilds using CCache (if installed): ``` $ PATH="/usr/lib/ccache:${PATH}" \ KBUILD_BUILD_TIMESTAMP=0 \ vng --build # or export the two variables before, see 'man ccache' for more details ``` - Execute `uname -r` inside a kernel recompiled in the current directory and send the output to cowsay on the host: ``` $ vng -- uname -r | cowsay __________________ < 6.1.0-rc6-virtme > ------------------ \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` - Run a bunch of parallel virtme-ng instances in a pipeline, with different kernels installed in the system, passing each other their stdout/stdin and return all the generated output back to the host (also measure the total elapsed time): ``` $ time true | \ > vng -r 5.19.0-38-generic -e "cat && uname -r" | \ > vng -r 6.2.0-19-generic -e "cat && uname -r" | \ > vng -r 6.2.0-20-generic -e "cat && uname -r" | \ > vng -r 6.3.0-2-generic -e "cat && uname -r" | \ > cowsay -n ___________________ / 5.19.0-38-generic \ | 6.2.0-19-generic | | 6.2.0-20-generic | \ 6.3.0-2-generic / ------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || real 0m2.737s user 0m8.425s sys 0m8.806s ``` - Run the vanilla v6.7-rc5 kernel with an Ubuntu 22.04 rootfs: ``` $ vng -r v6.7-rc5 --user root --root ./rootfs/22.04 --root-release jammy -- cat /etc/lsb-release /proc/version ... DISTRIB_ID=Ubuntu DISTRIB_RELEASE=22.04 DISTRIB_CODENAME=jammy DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS" Linux version 6.7.0-060700rc5-generic (kernel@kathleen) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.2.0-7ubuntu1) 13.2.0, GNU ld (GNU Binutils for Ubuntu) 2.41) #202312102332 SMP PREEMPT_DYNAMIC Sun Dec 10 23:41:31 UTC 2023 ``` - Run the current kernel creating a 1GB NUMA node with CPUs 0,1,3 assigned and a 3GB NUMA node with CPUs 2,4,5,6,7 assigned: ``` $ vng -r -m 4G --numa 1G,cpus=0-1,cpus=3 --numa 3G,cpus=2,cpus=4-7 -- numactl -H available: 2 nodes (0-1) node 0 cpus: 0 1 3 node 0 size: 1005 MB node 0 free: 914 MB node 1 cpus: 2 4 5 6 7 node 1 size: 2916 MB node 1 free: 2797 MB node distances: node 0 1 0: 10 20 1: 20 10 ``` - Run the current kernel creating 4 NUMA nodes of 1GB each and assign different distance costs between the NUMA nodes to simulate non-uniform memory access: ``` $ vng -r --cpu 8 -m 4G \ > --numa 1G,cpus=0-1 --numa 1G,cpus=2-3 \ > --numa 1G,cpus=4-5 --numa 1G,cpus=6-7 \ > --numa-distance 0,1=51 --numa-distance 0,2=31 --numa-distance 0,3=41 \ > --numa-distance 1,2=21 --numa-distance 1,3=61 \ > --numa-distance 2,3=11 -- numactl -H available: 4 nodes (0-3) node 0 cpus: 0 1 node 0 size: 1006 MB node 0 free: 974 MB node 1 cpus: 2 3 node 1 size: 953 MB node 1 free: 919 MB node 2 cpus: 4 5 node 2 size: 943 MB node 2 free: 894 MB node 3 cpus: 6 7 node 3 size: 1006 MB node 3 free: 965 MB node distances: node 0 1 2 3 0: 10 51 31 41 1: 51 10 21 61 2: 31 21 10 11 3: 41 61 11 10 ``` - Run `glxgears` inside a kernel recompiled in the current directory: ``` $ vng -g -- glxgears (virtme-ng is started in graphical mode) ``` - Execute an `awesome` window manager session with kernel 6.2.0-1003-lowlatency (installed in the system): ``` $ vng -r 6.2.0-1003-lowlatency -g -- awesome (virtme-ng is started in graphical mode) ``` - Run the `steam` snap (tested in Ubuntu) inside a virtme-ng instance using the 6.2.0-1003-lowlatency kernel: ``` $ vng -r 6.2.0-1003-lowlatency --snaps --net user -g -- /snap/bin/steam (virtme-ng is started in graphical mode) ``` - Generate a memory dump of a running instance and read 'jiffies' from the memory dump using the drgn debugger: ``` # Start the vng instance in debug mode $ vng --debug # In a separate shell session trigger the memory dump to /tmp/vmcore.img $ vng --dump /tmp/vmcore.img # Use drgn to read 'jiffies' from the memory dump: $ echo "print(prog['jiffies'])" | drgn -q -s vmlinux -c /tmp/vmcore.img drgn 0.0.23 (using Python 3.11.6, elfutils 0.189, with libkdumpfile) For help, type help(drgn). >>> import drgn >>> from drgn import NULL, Object, cast, container_of, execscript, offsetof, reinterpret, sizeof >>> from drgn.helpers.common import * >>> from drgn.helpers.linux import * >>> (volatile unsigned long)4294675464 ``` - Attach a gdb session to a running instance started with `--debug`: ``` # Start the vng instance in debug mode $ vng --debug # In a separate terminal run the following command to attach the gdb session: $ vng --gdb kernel version = 6.9.0-virtme Reading symbols from vmlinux... Remote debugging using localhost:1234 native_irq_disable () at ./arch/x86/include/asm/irqflags.h:37 37 asm volatile("cli": : :"memory"); (gdb) # NOTE: a vmlinux must be present in the current working directory in order # to resolve symbols, otherwise vng # will automatically search for a # vmlinux available in the system. ``` - Connect to a simple remote shell (`socat` is required, VSOCK will be used): ``` # Start the vng instance with server support: $ vng --console # In a separate terminal run the following command to connect to a remote shell: $ vng --console-client ``` - Enable ssh in the vng guest: ``` # Start the vng instance with ssh server support: $ vng --ssh # Connect to the vng guest from the host via ssh: $ vng --ssh-client ``` - Generate some results inside the vng guest and copy them back to the host using scp: ``` # Start the vng instance with SSH server support: arighi@host~> vng --ssh ... arighi@virtme-ng~> ./run.sh > result.txt # In another terminal, copy result.txt from the guest to the host using scp: arighi@host~> scp -F ~/.cache/virtme-ng/.ssh/virtme-ng-ssh.conf virtme-ng%2222:~/result.txt . # The SSH command can be printed using this command, and easily adapted later: arighi@host~> vng --ssh-client --dry-run ssh -F /home/arighi/.cache/virtme-ng/.ssh/virtme-ng-ssh.conf virtme-ng%2222 # With systemd >= 256, it is possible to use the 'vsock/' hostname directly: arighi@host~> ssh vsock/2222 arighi@virtme-ng~> ``` - Run virtme-ng inside a docker container: ``` $ docker run -it --privileged ubuntu:23.10 /bin/bash # apt update # echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections # apt install --yes git qemu-kvm udev iproute2 busybox-static \ coreutils python3-requests python3-argcomplete libvirt-clients kbd kmod file rsync zstd virtiofsd # git clone --recursive https://github.com/arighi/virtme-ng.git # ./virtme-ng/vng -r v6.6 -- uname -r 6.6.0-060600-generic ``` See also: `.github/workflows/run.yml` as a practical example on how to use virtme-ng inside docker. - Run virtme-ng with gpu passthrough: ``` # Confirm host kernel has VFIO and IOMMU support # Check if NVIDIA module is installed on the host $ modinfo nvidia # If the nvidia module is installed, blacklist the nvidia modules $ sudo bash -c 'echo -e "blacklist nvidia\nblacklist nvidia-drm\nblacklist nvidia-modeset\nblacklist nvidia-peermem\nblacklist nvidia-uvm" > /etc/modprobe.d/blacklist-nvidia.conf' # Host will need to be rebooted for blacklist to take effect. # Get GPU device ID $ lspci -nn | grep NVIDIA 0000:01:00.0 VGA compatible controller [0300]: NVIDIA Corporation AD104GLM [RTX 3500 Ada Generation Laptop GPU] [10de:27bb] (rev a1) 0000:01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:22bc] (rev a1)) # Configure VFIO for device passthrough $ sudo bash -c 'options vfio-pci ids=10de:27bb,10de:22bc' > /etc/modprobe.d/vfio.conf # Load VFIO module $ sudo modprobe vfio-pci # Pass PCI address to virtme-ng $ sudo vng --nvgpu "01:00.0" -r linux ``` Implementation details ====================== virtme-ng allows to automatically configure, build and run kernels using the main command-line interface called `vng`. A minimal custom `.config` is automatically generated if not already present when `--build` is specified. It is possible to specify a set of custom configs (.config chunk) in `~/.config/virtme-ng/kernel.config`, or using --config chunk-file's or --configitem CONFIG_FOO=bar's. These user-specific settings will successively override the default settings. The final overrides are the mandatory config items that are required to boot and test the kernel inside qemu, using `virtme-run`. Then the kernel is compiled either locally or on an external build host (if the `--build-host` option is used); once the build is done only the required files needed to test the kernel are copied from the remote host if an external build host is used. When a remote build host is used (`--build-host`) the target branch is force pushed to the remote host inside the `~/.virtme` directory. Then the kernel is executed using the virtme module. This allows to test the kernel using a safe copy-on-write snapshot of the entire host filesystem. All the kernels compiled with virtme-ng have a `-virtme` suffix to their kernel version, this allows to easily determine if you're inside a virtme-ng kernel or if you're using the real host kernel (simply by checking `uname -r`). External kernel modules ======================= It is possible to recompile and test out-of-tree kernel modules inside the virtme-ng kernel, simply by building them against the local directory of the kernel git repository that was used to build and run the kernel. Default options =============== Typically, if you always use virtme-ng with an external build server (e.g., `vng --build --build-host REMOTE_SERVER --build-host-exec-prefix CMD`) you don't always want to specify these options, so instead, you can simply define them in `~/.config/virtme-ng/virtme-ng.conf` under `default_opts` and then simply run `vng --build`. Example (always use an external build server called 'kathleen' and run make inside a build chroot called `chroot:lunar-amd64`). To do so, add the `default_opts` section in `~/.config/virtme-ng/virtme-ng.conf` as following: ``` { "default_opts": { "build_host": "kathleen", "build_host_exec_prefix": "schroot -c chroot:lunar-amd64 --" }, } ``` Now you can simply run `vng --build` to build your kernel from the current working directory using the external build host, prepending the exec prefix command when running make. Troubleshooting =============== - If you get permission denied when starting qemu, make sure that your username is assigned to the group `kvm` or `libvirt`: ``` $ groups | grep "kvm\|libvirt" ``` - When using `--network bridge` to create a bridged network in the guest you may get the following error: ``` ... failed to create tun device: Operation not permitted ``` This is because `qemu-bridge-helper` requires `CAP_NET_ADMIN` permissions. To fix this you need to add `allow all` to `/etc/qemu/bridge.conf` and set the `CAP_NET_ADMIN` capability to `qemu-bridge-helper`, as following: ``` $ sudo filecap /usr/lib/qemu/qemu-bridge-helper net_admin ``` - If the guest fails to start because the host doesn't have enough memory available you can specify a different amount of memory using `--memory MB`, (this option is passed directly to qemu via `-m`, default is 1G). - If you're testing a kernel for an architecture different than the host, keep in mind that you need to use also `--root DIR` to use a specific chroot with the binaries compatible with the architecture that you're testing. If the chroot doesn't exist in your system virtme-ng will automatically create it using the latest daily build Ubuntu cloud image: ``` $ vng --build --arch riscv64 --root ./tmproot ``` - If the build on a remote build host is failing unexpectedly you may want to try cleaning up the remote git repository, running: ``` $ vng --clean --build-host HOSTNAME ``` - Snap support is still experimental and something may not work as expected (keep in mind that virtme-ng will try to run snapd in a bare minimum system environment without systemd), if some snaps are not running try to disable apparmor, adding `--append="apparmor=0"` to the virtme-ng command line. - Running virtme-ng instances inside docker: in case of failures/issues, especially with stdin/stdout/stderr redirections, make sure that you have `udev` installed in your docker image and run the following command before using `vng`: ``` $ udevadm trigger --subsystem-match --action=change ``` - To mount the legacy cgroup filesystem (v1) layout, add `SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1` to the kernel boot options: ``` $ vng -r --append "SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1" -- 'df -T /sys/fs/cgroup/*' Filesystem Type 1K-blocks Used Available Use% Mounted on blkio cgroup 0 0 0 - /sys/fs/cgroup/blkio cpu cgroup 0 0 0 - /sys/fs/cgroup/cpu cpuacct cgroup 0 0 0 - /sys/fs/cgroup/cpuacct devices cgroup 0 0 0 - /sys/fs/cgroup/devices memory cgroup 0 0 0 - /sys/fs/cgroup/memory pids cgroup 0 0 0 - /sys/fs/cgroup/pids ``` Contributing ============ Please see DCO-1.1.txt. Additional resources ==================== - [LWN: Faster kernel testing with virtme-ng (November, 2023)](https://lwn.net/Articles/951313/) - [LPC 2023: Speeding up Kernel Testing and Debugging with virtme-ng](https://lpc.events/event/17/contributions/1506/attachments/1143/2441/virtme-ng.pdf) - [Kernel Recipes 2024: virtme-ng](https://kernel-recipes.org/en/2024/virtme-ng/) - [Linux Foundation Mentorship Session: Speeding Up Kernel Development With virtme-ng](https://www.youtube.com/watch?v=ZgMLGM2UazY) Credits ======= virtme-ng is written by Andrea Righi virtme-ng is based on virtme, written by Andy Lutomirski ([web][korg-web] | [git][korg-git]). [korg-web]: https://git.kernel.org/cgit/utils/kernel/virtme/virtme.git "virtme on kernel.org" [korg-git]: git://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git "git address" [virtme]: https://github.com/amluto/virtme "virtme" [virtme-ng-ppa]: https://launchpad.net/~arighi/+archive/ubuntu/virtme-ng "virtme-ng ppa" virtme-ng-1.35/bin/000077500000000000000000000000001500572323100141205ustar00rootroot00000000000000virtme-ng-1.35/bin/virtme-prep-kdir-mods000077500000000000000000000047201500572323100202120ustar00rootroot00000000000000#!/bin/sh # This is still a bit of an experiment. FAKEVER=0.0.0 DIR=".virtme_mods" MODDIR_USRMERGE="$DIR/usr" MODDIR="$DIR/lib/modules/$FAKEVER" # Some distro don't have /sbin or /usr/sbin in user's default path. Make sure # to setup the right path to find all the commands needed to setup the modules # (depmod, etc.). PATH=$PATH:/sbin:/usr/sbin COPY_MODULES=${COPY_MODULES:-"false"} print_help() { script_name=$(basename "$0") echo "usage: ${script_name} [-h | --help] [-c | --copy-modules]" echo "" echo "optional arguments:" echo " -h, --help show this help message and exit" echo " -c, --copy-modules copy kernel instead of linking" } while ":"; do case "$1" in -h | --help) print_help exit 0 ;; -c | --copy-modules) COPY_MODULES="true" shift ;; *) break ;; esac done if ! [ -f "modules.order" ]; then echo 'virtme-prep-kdir-mods must be run from a kernel build directory' >&2 echo "modules.order is missing. Your kernel may be too old or you didn't make modules." >&2 exit 1 fi # Delete existing .virtme_modes/lib/modules/0.0.0/modules.dep file at the beginning, # and regenerated by depmod at the end. So if we are interrupted during the # preparation of .virtme_mods folder, the next run command can correctly trigger the # preparation work again. rm -f -- "$MODDIR/modules.dep" # Set up .virtme_mods/lib/modules/0.0.0 as a module directory for this kernel, # but fill it with symlinks instead of actual modules. mkdir -p "$MODDIR/kernel" ln -srfT . "$MODDIR/build" # depmod can expect kernel modules to live on /usr/lib/modules on distributions # that are making use of usrmerge. ln -srfT "$DIR" "$MODDIR_USRMERGE" # Remove all preexisting symlinks and add symlinks to all modules that belong # to the build kenrnel. find "$MODDIR/kernel" -type l -print0 | xargs -0 rm -f -- # from v6.2, modules.order lists .o files, we need the .ko ones sed 's:\.o$:.ko:' modules.order | while read -r i; do [ ! -e "$i" ] && i=$(echo "$i" | sed s:^kernel/::) mkdir -p "$MODDIR/kernel/$(dirname "$i")" if [ "$COPY_MODULES" = "true" ]; then cp "$i" "$MODDIR/kernel/$i" else ln -sr "$i" "$MODDIR/kernel/$i" fi done # Link in the files that make modules_install would copy ln -srf modules.builtin modules.builtin.modinfo modules.order "$MODDIR/" # Now run depmod to collect dependencies depmod -ae -F System.map -b .virtme_mods "$FAKEVER" virtme-ng-1.35/bin/virtme-ssh-proxy000077500000000000000000000042561500572323100173350ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import array import socket import sys from typing import Iterable, Optional # Supported hostname and CID separators # # Important: Keep in sync with `VIRTME_SSH_HOSTNAME_CID_SEPARATORS` SEPARATORS = ("%", "/") # Some exit codes SUCCESS = 0 ERROR_USAGE = 2 def get_hostname_cid(ssh_dst: str) -> tuple[Optional[str], Optional[int]]: """Return the hostname and CID >>> hostname, cid = get_hostname_cid("virtme-ng/24") >>> hostname 'virtme-ng' >>> cid 24 >>> hostname, cid = get_hostname_cid("virtme/ng/21") >>> hostname 'virtme/ng' >>> cid 21 >>> hostname, cid = get_hostname_cid("v/ng,20") >>> hostname 'v/ng' >>> cid 20 """ for sep in SEPARATORS: if sep not in ssh_dst: continue splitted = ssh_dst.rsplit(sep, 1) try: hostname, cid = splitted[0], int(splitted[1], 10) except ValueError: print(f"No integer given for CID: {splitted[1]}", file=sys.stderr) return (None, None) return hostname, cid print( f"SSH destination name includes no CID: {ssh_dst}. For example 'virtme-ng/30'", file=sys.stderr, ) return (None, None) # See https://docs.python.org/3/library/socket.html#socket.socket.sendmsg def send_fds(sock: socket.socket, fds: Iterable[int]) -> None: ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))] sock.sendmsg( [b"\0"], ancdata, socket.MSG_CMSG_CLOEXEC, ) def passfds(cid: int, port: int) -> None: sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM | socket.SOCK_CLOEXEC, 0) sock.connect((cid, port)) return send_fds(socket.socket(fileno=1), [sock.fileno()]) def main() -> int: parser = argparse.ArgumentParser( prog="virtme-connect", ) parser.add_argument("ssh_destination") parser.add_argument("-p", "--port", type=int, default=22) args = parser.parse_args() _hostname, cid = get_hostname_cid(args.ssh_destination) if cid is None: return ERROR_USAGE passfds(cid, args.port) return SUCCESS if __name__ == "__main__": sys.exit(main()) virtme-ng-1.35/cfg/000077500000000000000000000000001500572323100141075ustar00rootroot00000000000000virtme-ng-1.35/cfg/virtme-ng.conf000066400000000000000000000000411500572323100166610ustar00rootroot00000000000000{ "default_opts" : { } } virtme-ng-1.35/pyproject.toml000066400000000000000000000014751500572323100162730ustar00rootroot00000000000000[project] name = "virtme-ng" dynamic = ["version", "license", "authors", "dependencies", "readme", "description", "classifiers", "scripts"] requires-python = ">=3.8" [build-system] requires = [ "argparse-manpage[setuptools]", "setuptools", # Runtime dependencies, needed to generate manpages. "argcomplete", "requests", ] [tool.build_manpages] manpages = [ """\ man/vng.1\ :pyfile=virtme_ng/run.py\ :function=make_parser\ :author=virtme-ng is written by Andrea Righi \ :author=Based on virtme by Andy Lutomirski \ :manual_title=virtme-ng\ :description=Quickly run kernels inside a virtualized snapshot of your live system\ """, ] [tool.ruff.lint] select = [ # Pyflakes "F", # pyupgrade "UP", # flake8-bugbear "B", # isort "I", ] virtme-ng-1.35/setup.cfg000066400000000000000000000023061500572323100151720ustar00rootroot00000000000000[flake8] max_line_length = 120 per-file-ignores = # E402 module level import not at top of file annotations: E402 # See https://github.com/psf/black/issues/315 extend-ignore = E203 [pylint.FORMAT] max-line-length = 120 [pylint] # These are the default disables but for some reason we seem to loose them # due to the above statement (huh?). So redefine them. disable = invalid-name, missing-module-docstring, missing-class-docstring, missing-function-docstring, wrong-import-position, raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, too-many-instance-attributes, too-many-arguments, too-many-locals, too-many-statements, redefined-outer-name, # End of default disables similarities, too-many-branches, too-many-return-statements, too-few-public-methods, consider-using-f-string, consider-using-with, too-many-nested-blocks, too-many-lines virtme-ng-1.35/setup.py000077500000000000000000000131151500572323100150660ustar00rootroot00000000000000#!/usr/bin/env python3 import os import platform import subprocess import sys import sysconfig from argcomplete import shell_integration try: from build_manpages import build_manpages, get_build_py_cmd, get_install_cmd except ModuleNotFoundError: build_manpages = None from setuptools import setup from setuptools.command.build_py import build_py from setuptools.command.egg_info import egg_info from virtme_ng.version import get_version_string os.environ["__VNG_LOCAL"] = "1" VERSION = get_version_string() # Source .config if it exists (where we can potentially defined config/build # options) if os.path.exists(".config"): with open(".config", encoding="utf-8") as config_file: for line in config_file: key, value = line.strip().split("=") os.environ[key] = value # Global variables to store custom build options (as env variables) build_virtme_ng_init = int(os.environ.get("BUILD_VIRTME_NG_INIT", 0)) # Make sure virtme-ng-init submodule has been cloned if build_virtme_ng_init and not os.path.exists("virtme_ng_init/Cargo.toml"): sys.stderr.write( "WARNING: virtme-ng-init submodule not available, trying to clone it\n" ) subprocess.check_call("git submodule update --init --recursive", shell=True) # Always include standard site-packages to PYTHONPATH os.environ["PYTHONPATH"] = sysconfig.get_paths()["purelib"] # Produce static Rust binaries. # # This is required to use the same virtme-ng-init across different root # filesystems (when `--root DIR` is used). os.environ["RUSTFLAGS"] = "-C target-feature=+crt-static " + os.environ.get( "RUSTFLAGS", "" ) class BuildPy(build_py): def run(self): print(f"BUILD_VIRTME_NG_INIT: {build_virtme_ng_init}") # Build virtme-ng-init if build_virtme_ng_init: cwd = "virtme_ng_init" root = "../virtme/guest" args = ["cargo", "install", "--path", ".", "--root", root] if platform.system() == "Darwin": machine = platform.machine() if machine == "arm64": machine = "aarch64" target = f"{machine}-unknown-linux-musl" args.extend( [ "--target", target, "--config", f'target.{target}.linker = "rust-lld"', ] ) subprocess.check_call(args, cwd="virtme_ng_init") subprocess.check_call( ["strip", os.path.join(root, "bin", "virtme-ng-init")], cwd=cwd, ) # Generate bash autocompletion scripts with open("virtme-ng-prompt", "w", encoding="utf-8") as f: f.write(shell_integration.shellcode(["virtme-ng"])) with open("vng-prompt", "w", encoding="utf-8") as f: f.write(shell_integration.shellcode(["vng"])) # Run the rest of virtme-ng build build_py.run(self) class EggInfo(egg_info): def run(self): # Initialize virtme guest binary directory guest_bin_dir = "virtme/guest/bin" if not os.path.exists(guest_bin_dir): os.mkdir(guest_bin_dir) # Install guest binaries if build_virtme_ng_init and not os.path.exists( "virtme/guest/bin/virtme-ng-init" ): self.run_command("build") egg_info.run(self) packages = [ "virtme_ng", "virtme", "virtme.commands", "virtme.guest", ] package_files = [ "virtme-init", "virtme-udhcpc-script", "virtme-sshd-script", "virtme-ssh-proxy", "virtme-snapd-script", "virtme-sound-script", ] if build_virtme_ng_init: package_files.append("bin/virtme-ng-init") packages.append("virtme.guest.bin") data_files = [ ("/etc", ["cfg/virtme-ng.conf"]), ("/usr/share/bash-completion/completions", ["virtme-ng-prompt", "vng-prompt"]), ] if build_manpages: data_files.append(("/usr/share/man/man1", ["man/vng.1"])) cmdclass = { "egg_info": EggInfo, "build_py": BuildPy, } if build_manpages: cmdclass["build_manpages"] = build_manpages cmdclass["build_py"] = get_build_py_cmd(BuildPy) cmdclass["install"] = get_install_cmd() setup( name="virtme-ng", version=VERSION, author="Andrea Righi", author_email="arighi@nvidia.com", description="Build and run a kernel inside a virtualized snapshot of your live system", url="https://github.com/arighi/virtme-ng", license="GPLv2", long_description=open( os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8" ).read(), long_description_content_type="text/markdown", install_requires=[ "argcomplete", "requests", "setuptools", ], entry_points={ "console_scripts": [ "vng = virtme_ng.run:main", "virtme-ng = virtme_ng.run:main", "virtme-run = virtme.commands.run:main", "virtme-configkernel = virtme.commands.configkernel:main", "virtme-mkinitramfs = virtme.commands.mkinitramfs:main", ] }, cmdclass=cmdclass, packages=packages, package_data={"virtme.guest": package_files}, data_files=data_files, scripts=[ "bin/virtme-prep-kdir-mods", "bin/virtme-ssh-proxy", ], include_package_data=True, classifiers=[ "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Operating System :: POSIX :: Linux", ], zip_safe=False, ) virtme-ng-1.35/virtme-configkernel000077500000000000000000000010651500572323100172520ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- mode: python -*- # virtme-configkernel: Configure a kernel for virtme # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 # This file is not installed; it's just used to run virtme from inside # a source distribution. # NOTE: this command is deprecated, please use vng instead. import sys from virtme.commands import configkernel sys.exit(configkernel.main()) virtme-ng-1.35/virtme-mkinitramfs000077500000000000000000000011001500572323100171160ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- mode: python -*- # virtme-mkinitramfs: Generate an initramfs image for virtme # Copyright © 2019 Marcos Paulo de Souza # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 # This file is not installed; it's just use to run virtme from inside # a source distribution. # NOTE: this command is deprecated, please use vng instead. import sys from virtme.commands import mkinitramfs sys.exit(mkinitramfs.main()) virtme-ng-1.35/virtme-run000077500000000000000000000010431500572323100154040ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- mode: python -*- # virtme-run: The legacy command-line virtme frontend # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 # This file is not installed; it's just use to run virtme from inside # a source distribution. # NOTE: this command is deprecated, please use vng instead. import sys from virtme.commands import run sys.exit(run.main()) virtme-ng-1.35/virtme/000077500000000000000000000000001500572323100146565ustar00rootroot00000000000000virtme-ng-1.35/virtme/__init__.py000066400000000000000000000000001500572323100167550ustar00rootroot00000000000000virtme-ng-1.35/virtme/architectures.py000066400000000000000000000304031500572323100200750ustar00rootroot00000000000000# -*- mode: python -*- # qemu_helpers: Helpers to find QEMU and handle its quirks # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import os from typing import List, Optional class Arch: def __init__(self, name) -> None: self.virtmename = name self.qemuname = name self.linuxname = name self.gccname = name defconfig_target = "defconfig" @staticmethod def virtiofs_support() -> bool: return False @staticmethod def numa_support() -> bool: return True @staticmethod def qemuargs(is_native, use_kvm, use_gpu) -> List[str]: _ = is_native _ = use_kvm _ = use_gpu return [] @staticmethod def virtio_dev_type(virtiotype) -> str: # Return a full name for a virtio device. It would be # nice if QEMU abstracted this away, but it doesn't. return f"virtio-{virtiotype}-pci" @staticmethod def vhost_dev_type(vhosttype) -> str: return f"vhost-{vhosttype}-pci" @staticmethod def earlyconsole_args() -> List[str]: return [] @staticmethod def serial_console_args() -> List[str]: return [] @staticmethod def qemu_nodisplay_args() -> List[str]: return ["-vga", "none", "-display", "none"] @staticmethod def qemu_nodisplay_nvgpu_args() -> List[str]: return ["-display", "none"] @staticmethod def qemu_display_args() -> List[str]: return ["-device", "virtio-gpu-pci"] @staticmethod def qemu_sound_args() -> List[str]: return [] @staticmethod def qemu_vmcoreinfo_args() -> List[str]: return ["-device", "vmcoreinfo"] @staticmethod def qemu_serial_console_args() -> List[str]: # We should be using the new-style -device serialdev,chardev=xyz, # but many architecture-specific serial devices don't support that. return ["-serial", "chardev:console"] @staticmethod def config_base() -> List[str]: return [] def kimg_path(self) -> str: return f"arch/{self.linuxname}/boot/bzImage" def img_name(self) -> List[str]: return ["vmlinuz"] @staticmethod def dtb_path() -> Optional[str]: return None class Arch_unknown(Arch): @staticmethod def qemuargs(is_native, use_kvm, use_gpu): return Arch.qemuargs(is_native, use_kvm, use_gpu) class Arch_x86(Arch): def __init__(self, name): Arch.__init__(self, name) self.linuxname = "x86" self.defconfig_target = f"{name}_defconfig" @staticmethod def virtiofs_support() -> bool: return True @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) # Add a watchdog. This is useful for testing. ret.extend(["-device", "i6300esb,id=watchdog0"]) if is_native and use_kvm: # If we're likely to use KVM, request a full-featured CPU. # (NB: if KVM fails, this will cause problems. We should probe.) cpu_str = "host" if use_gpu: cpu_str += ",host-phys-bits-limit=0x28" ret.extend(["-cpu", cpu_str]) else: ret.extend(["-machine", "q35"]) return ret @staticmethod def qemu_sound_args() -> List[str]: return [ "-audiodev", "sdl,id=snd0", "-device", "intel-hda", "-device", "hda-output,audiodev=snd0", ] @staticmethod def earlyconsole_args(): return ["earlyprintk=serial,ttyS0,115200"] @staticmethod def serial_console_args(): return ["ttyS0"] @staticmethod def config_base(): return [ "CONFIG_SERIO=y", "CONFIG_PCI=y", "CONFIG_INPUT=y", "CONFIG_INPUT_KEYBOARD=y", "CONFIG_KEYBOARD_ATKBD=y", "CONFIG_SERIAL_8250=y", "CONFIG_SERIAL_8250_CONSOLE=y", "CONFIG_X86_VERBOSE_BOOTUP=y", "CONFIG_VGA_CONSOLE=y", "CONFIG_FB=y", "CONFIG_FB_VESA=y", "CONFIG_FRAMEBUFFER_CONSOLE=y", "CONFIG_RTC_CLASS=y", "CONFIG_RTC_HCTOSYS=y", "CONFIG_RTC_DRV_CMOS=y", "CONFIG_HYPERVISOR_GUEST=y", "CONFIG_PARAVIRT=y", "CONFIG_KVM_GUEST=y", # Depending on the host kernel, virtme can nest! "CONFIG_KVM=y", "CONFIG_KVM_INTEL=y", "CONFIG_KVM_AMD=y", ] class Arch_microvm(Arch_x86): @staticmethod def virtio_dev_type(virtiotype): return f"virtio-{virtiotype}-device" @staticmethod def vhost_dev_type(vhosttype) -> str: return f"vhost-{vhosttype}-device" @staticmethod def qemu_display_args() -> List[str]: return [ "-device", "virtio-keyboard-device", "-device", "virtio-tablet-device", "-device", "virtio-gpu-device", "-global", "virtio-mmio.force-legacy=false", ] @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) # Use microvm architecture for faster boot ret.extend(["-M", "microvm,accel=kvm,pcie=on,rtc=on"]) if is_native and use_kvm: # If we're likely to use KVM, request a full-featured CPU. # (NB: if KVM fails, this will cause problems. We should probe.) ret.extend(["-cpu", "host"]) # We can't migrate regardless. return ret class Arch_arm(Arch): def __init__(self): Arch.__init__(self, "arm") self.defconfig_target = "vexpress_defconfig" @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) # Emulate a vexpress-a15. ret.extend(["-M", "vexpress-a15"]) # NOTE: consider adding a PCI bus (and figuring out how) # # This won't boot unless -dtb is set, but we need to figure out # how to find the dtb file. return ret @staticmethod def qemu_display_args() -> List[str]: return ["-device", "virtio-gpu-device"] @staticmethod def virtio_dev_type(virtiotype): return f"virtio-{virtiotype}-device" @staticmethod def earlyconsole_args(): return ["earlyprintk=serial,ttyAMA0,115200"] @staticmethod def serial_console_args(): return ["ttyAMA0"] def kimg_path(self): return "arch/arm/boot/zImage" @staticmethod def dtb_path(): if os.path.exists("arch/arm/boot/dts/arm/vexpress-v2p-ca15-tc1.dtb"): return "arch/arm/boot/dts/arm/vexpress-v2p-ca15-tc1.dtb" if os.path.exists("arch/arm/boot/dts/vexpress-v2p-ca15-tc1.dtb"): return "arch/arm/boot/dts/vexpress-v2p-ca15-tc1.dtb" return None class Arch_aarch64(Arch): def __init__(self, name): Arch.__init__(self, name) self.qemuname = "aarch64" self.linuxname = "arm64" self.gccname = "aarch64" @staticmethod def virtiofs_support() -> bool: return True @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) if is_native and use_kvm: ret.extend(["-M", "virt,gic-version=host"]) ret.extend(["-cpu", "host"]) else: # Emulate a fully virtual system. ret.extend(["-M", "virt"]) # Despite being called qemu-system-aarch64, QEMU defaults to # emulating a 32-bit CPU. Override it. ret.extend(["-cpu", "cortex-a57"]) return ret @staticmethod def virtio_dev_type(virtiotype): return f"virtio-{virtiotype}-device" @staticmethod def earlyconsole_args(): return ["earlyprintk=serial,ttyAMA0,115200"] @staticmethod def serial_console_args(): return ["ttyAMA0"] def kimg_path(self): return "arch/arm64/boot/Image" class Arch_ppc(Arch): def __init__(self, name): Arch.__init__(self, name) self.defconfig_target = "pseries_defconfig" self.qemuname = "ppc64" self.linuxname = "powerpc" self.gccname = "powerpc64le" @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) ret.extend(["-M", "pseries"]) return ret @staticmethod def config_base(): return [ "CONFIG_CPU_LITTLE_ENDIAN=y", "CONFIG_PPC_POWERNV=n", "CONFIG_PPC_SUBPAGE_PROT=y", "CONFIG_KVM_BOOK3S_64=y", "CONFIG_ZONE_DEVICE=y", ] def kimg_path(self): # Apparently SLOF (QEMU's bundled firmware?) can't boot a zImage. return "vmlinux" def img_name(self) -> List[str]: return ["vmlinux"] class Arch_riscv64(Arch): def __init__(self): Arch.__init__(self, "riscv64") self.defconfig_target = "defconfig" self.linuxname = "riscv" @staticmethod def virtiofs_support() -> bool: return True @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) ret.extend(["-machine", "virt"]) ret.extend(["-bios", "default"]) return ret @staticmethod def serial_console_args(): return ["ttyS0"] def kimg_path(self): return "arch/riscv/boot/Image" class Arch_sparc64(Arch): def __init__(self): Arch.__init__(self, "sparc64") self.defconfig_target = "sparc64_defconfig" self.linuxname = "sparc" @staticmethod def qemuargs(is_native, use_kvm, use_gpu): return Arch.qemuargs(is_native, use_kvm, use_gpu) def kimg_path(self): return "arch/sparc/boot/image" @staticmethod def qemu_nodisplay_args(): # qemu-system-sparc fails to boot if -display none is set. return ["-nographic", "-vga", "none"] class Arch_s390x(Arch): def __init__(self): Arch.__init__(self, "s390x") self.linuxname = "s390" @staticmethod def virtiofs_support() -> bool: return True @staticmethod def numa_support() -> bool: return False @staticmethod def virtio_dev_type(virtiotype): return f"virtio-{virtiotype}-ccw" @staticmethod def vhost_dev_type(vhosttype) -> str: return f"vhost-{vhosttype}-ccw" @staticmethod def qemuargs(is_native, use_kvm, use_gpu): ret = Arch.qemuargs(is_native, use_kvm, use_gpu) # Ask for the latest version of s390-ccw ret.extend(["-M", "s390-ccw-virtio"]) if is_native and use_kvm: ret.extend(["-cpu", "host"]) # Add a watchdog. This is useful for testing. ret.extend(["-device", "diag288,id=watchdog0"]) # To be able to configure a console, we need to get rid of the # default console ret.extend(["-nodefaults"]) return ret @staticmethod def qemu_display_args() -> List[str]: return ["-device", "virtio-gpu-ccw,devno=fe.0.0101"] @staticmethod def config_base(): return ["CONFIG_MARCH_Z900=y", "CONFIG_DIAG288_WATCHDOG=y"] @staticmethod def serial_console_args() -> List[str]: return ["ttysclp0"] @staticmethod def qemu_serial_console_args(): return ["-device", "sclpconsole,chardev=console"] @staticmethod def earlyconsole_args() -> List[str]: return ["earlyprintk=sclp"] @staticmethod def qemu_vmcoreinfo_args() -> List[str]: return [] def img_name(self) -> List[str]: return ["vmlinuz", "image"] ARCHES = { arch.virtmename: arch for arch in [ Arch_microvm("microvm"), Arch_x86("x86_64"), Arch_x86("i386"), Arch_arm(), Arch_aarch64("aarch64"), Arch_aarch64("arm64"), Arch_ppc("ppc64"), Arch_ppc("ppc64le"), Arch_riscv64(), Arch_sparc64(), Arch_s390x(), ] } def get(arch: str) -> Arch: if arch in ARCHES: return ARCHES[arch] return Arch_unknown(arch) virtme-ng-1.35/virtme/commands/000077500000000000000000000000001500572323100164575ustar00rootroot00000000000000virtme-ng-1.35/virtme/commands/__init__.py000066400000000000000000000000001500572323100205560ustar00rootroot00000000000000virtme-ng-1.35/virtme/commands/configkernel.py000066400000000000000000000304431500572323100215030ustar00rootroot00000000000000# -*- mode: python -*- # virtme-configkernel: Configure a kernel for virtme # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import argparse import multiprocessing import os import platform import shlex import shutil import subprocess import sys from typing import Optional from .. import architectures from ..util import SilentError def check_file_arg(filepath): if not os.path.isfile(filepath): raise argparse.ArgumentTypeError(f"'{filepath}' is not a valid file.") return filepath def make_parser(): parser = argparse.ArgumentParser( description="Configure a kernel for virtme", ) parser.add_argument( "--arch", action="store", metavar="ARCHITECTURE", default=platform.machine(), help="Target architecture", ) parser.add_argument( "--cross-compile", action="store", metavar="CROSS_COMPILE_PREFIX", help="Cross-compile compiler prefix", ) parser.add_argument( "--custom", action="append", type=check_file_arg, metavar="CUSTOM", help="Use a custom config snippet file to override specific config options", ) parser.add_argument( "--configitem", action="append", metavar="CONFITEM", help="add or alter a (CONFIG_)?FOO=bar item", ) parser.add_argument( "--no-update", action="store_true", help="Skip if the config file already exists", ) parser.add_argument( "--verbose", action="store_true", help="get chatty about config assembled", ) g = parser.add_argument_group(title="Mode").add_mutually_exclusive_group() g.add_argument( "--allnoconfig", action="store_true", help="Overwrite configuration with a virtme-suitable allnoconfig (unlikely to work)", ) g.add_argument( "--defconfig", action="store_true", help="Overwrite configuration with a virtme-suitable defconfig", ) g.add_argument( "--update", action="store_true", help="Update existing config for virtme" ) parser.add_argument( "envs", metavar="envs", type=str, nargs="*", help="Additional Makefile variables", ) return parser _ARGPARSER = make_parser() def arg_fail(message): print(message) _ARGPARSER.print_usage() sys.exit(1) _GENERIC_CONFIG = [ "##: Generic", "CONFIG_UEVENT_HELPER=n", # Obsolete and slow "CONFIG_VIRTIO=y", "CONFIG_VIRTIO_PCI=y", "CONFIG_VIRTIO_MMIO=y", "CONFIG_VIRTIO_BALLOON=y", "CONFIG_NET=y", "CONFIG_NET_CORE=y", "CONFIG_NETDEVICES=y", "CONFIG_NETWORK_FILESYSTEMS=y", "CONFIG_INET=y", "CONFIG_NET_9P=y", "CONFIG_NET_9P_VIRTIO=y", "CONFIG_9P_FS=y", "CONFIG_VIRTIO_NET=y", "CONFIG_CMDLINE_OVERRIDE=n", "CONFIG_BINFMT_SCRIPT=y", "CONFIG_SHMEM=y", "CONFIG_TMPFS=y", "CONFIG_UNIX=y", "CONFIG_MODULE_SIG_FORCE=n", "CONFIG_DEVTMPFS=y", "CONFIG_TTY=y", "CONFIG_VT=y", "CONFIG_UNIX98_PTYS=y", "CONFIG_EARLY_PRINTK=y", "CONFIG_INOTIFY_USER=y", "", "##: virtio-scsi support", "CONFIG_BLOCK=y", "CONFIG_SCSI_LOWLEVEL=y", "CONFIG_SCSI=y", "CONFIG_SCSI_VIRTIO=y", "CONFIG_BLK_DEV_SD=y", "", "##: virt-serial support", "CONFIG_VIRTIO_CONSOLE=y", "", "##: watchdog (useful for test scripts)", "CONFIG_WATCHDOG=y", "CONFIG_WATCHDOG_CORE=y", "CONFIG_I6300ESB_WDT=y", "##: Make sure debuginfo are available", "CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y", "##: Enable overlayfs", "CONFIG_OVERLAY_FS=y", "##: virtio-fs support", "CONFIG_DAX=y", "CONFIG_DAX_DRIVER=y", "CONFIG_FS_DAX=y", "CONFIG_MEMORY_HOTPLUG=y", "CONFIG_MEMORY_HOTREMOVE=y", "CONFIG_ZONE_DEVICE=y", "CONFIG_FUSE_FS=y", "CONFIG_VIRTIO_FS=y", "##: vsock support", "CONFIG_VSOCKETS=y", "CONFIG_VIRTIO_VSOCKETS=y", ] _GENERIC_CONFIG_OPTIONAL = [ "##: initramfs support", "CONFIG_BLK_DEV_INITRD=y", "##: BPF stuff & useful debugging features", "CONFIG_BPF=y", "CONFIG_BPF_SYSCALL=y", "CONFIG_BPF_JIT=y", "CONFIG_HAVE_EBPF_JIT=y", "CONFIG_BPF_EVENTS=y", "CONFIG_FTRACE_SYSCALLS=y", "CONFIG_FUNCTION_TRACER=y", "CONFIG_HAVE_DYNAMIC_FTRACE=y", "CONFIG_DYNAMIC_FTRACE=y", "CONFIG_HAVE_KPROBES=y", "CONFIG_KPROBES=y", "CONFIG_KPROBE_EVENTS=y", "CONFIG_ARCH_SUPPORTS_UPROBES=y", "CONFIG_UPROBES=y", "CONFIG_UPROBE_EVENTS=y", "CONFIG_DEBUG_FS=y", "##: Required to generate memory dumps for drgn", "CONFIG_FW_CFG_SYSFS=y", "CONFIG_FW_CFG_SYSFS_CMDLINE=y", "##: Graphics support", "CONFIG_DRM=y", "CONFIG_DRM_VIRTIO_GPU=y", "CONFIG_DRM_VIRTIO_GPU_KMS=y", "CONFIG_DRM_BOCHS=y", "CONFIG_VIRTIO_IOMMU=y", "##: Sound support", "CONFIG_SOUND=y", "CONFIG_SND=y", "CONFIG_SND_SEQUENCER=y", "CONFIG_SND_PCI=y", "CONFIG_SND_INTEL8X0=y", "CONFIG_SND_HDA_CODEC_REALTEK=y", "# CONFIG_SND_DRIVERS is not set", "# CONFIG_SND_X86 is not set", "# CONFIG_SND_PCMCIA is not set", "# Required to run snaps", "CONFIG_SECURITYFS=y", "CONFIG_CGROUP_BPF=y", "CONFIG_SQUASHFS=y", "CONFIG_SQUASHFS_XZ=y", "CONFIG_SQUASHFS_ZSTD=y", "CONFIG_FUSE_FS=y", "# Unnecessary configs", "# CONFIG_LOCALVERSION_AUTO is not set", "# CONFIG_TEST_KMOD is not set", "# CONFIG_USB is not set", "# CONFIG_CAN is not set", "# CONFIG_BLUETOOTH is not set", "# CONFIG_I2C is not set", "# CONFIG_USB_HID is not set", "# CONFIG_HID is not set", "# CONFIG_TIGON3 is not set", "# CONFIG_BNX2X is not set", "# CONFIG_CHELSIO_T1 is not set", "# CONFIG_BE2NET is not set", "# CONFIG_S2IO is not set", "# CONFIG_EHEA is not set", "# CONFIG_E100 is not set", "# CONFIG_IXGB is not set", "# CONFIG_IXGBE is not set", "# CONFIG_I40E is not set", "# CONFIG_MLX4_EN is not set", "# CONFIG_MLX5_CORE is not set", "# CONFIG_MYRI10GE is not set", "# CONFIG_NETXEN_NIC is not set", "# CONFIG_NFS_FS is not set", "# CONFIG_IPV6 is not set", "# CONFIG_AUDIT is not set", "# CONFIG_SECURITY is not set", "# CONFIG_WIRELESS is not set", "# CONFIG_WLAN is not set", "# CONFIG_SCHED_MC is not set", "# CONFIG_CPU_FREQ is not set", "# CONFIG_INFINIBAND is not set", "# CONFIG_PPP is not set", "# CONFIG_PPPOE is not set", "# CONFIG_EXT2_FS is not set", "# CONFIG_REISERFS_FS not set", "# CONFIG_JFS_FS is not set", "# CONFIG_XFS_FS is not set", "# CONFIG_BTRFS_FS is not set", "# CONFIG_HFS_FS is not set", "# CONFIG_HFSPLUS_FS is not set", "# CONFIG_SCSI_FC_ATTRS is not set", "# CONFIG_SCSI_CXGB3_ISCSI is not set", "# CONFIG_SCSI_CXGB4_ISCSI is not set", "# CONFIG_SCSI_BNX2_ISCSI is not set", "# CONFIG_BE2ISCSI is not set", "# CONFIG_SCSI_MPT2SAS is not set", "# CONFIG_SCSI_IBMVFC is not set", "# CONFIG_SCSI_SYM53C8XX_2 is not set", "# CONFIG_SCSI_IPR is not set", "# CONFIG_SCSI_QLA_FC is not set", "# CONFIG_SCSI_QLA_ISCSI is not set", "# CONFIG_SCSI_DH is not set", "# CONFIG_FB_MATROX is not set", "# CONFIG_FB_RADEON is not set", "# CONFIG_FB_IBM_GXT4500 is not set", "# CONFIG_FB_VESA is not set", "# CONFIG_YENTA is not set", "# CONFIG_NETFILTER is not set", "# CONFIG_RFKILL is not set", "# CONFIG_ETHERNET is not set", "# CONFIG_BLK_DEV_SR is not set", "# CONFIG_TCP_MD5SIG is not set", "# CONFIG_XFRM_USER is not set", "# CONFIG_CRYPTO is not set", "# CONFIG_EXT4_FS is not set", "# CONFIG_VFAT_FS is not set", "# CONFIG_FAT_FS is not set", "# CONFIG_MSDOS_FS is not set", "# CONFIG_AUTOFS4_FS is not set", "# CONFIG_AUTOFS_FS is not set", "# CONFIG_NVRAM is not set", ] def do_it(): args = _ARGPARSER.parse_args() arch = architectures.get(args.arch) is_native = args.arch == platform.machine() # Determine if an initial config is present config = ".config" makef = "Makefile" # 1st check if make O= arg or KBUILD_OUTPUT is defined and if # it's a directory; it would have the .config and Makefile # make "O=..." takes precedence over KBUILD_OUTPUT. for var in args.envs: if var.startswith("O="): os.environ["KBUILD_OUTPUT"] = var[2:] config_dir = os.environ.get("KBUILD_OUTPUT") if config_dir is not None: try: os.makedirs(config_dir, exist_ok=True) except Exception as exc: print(f"Error: invalid directory for KBUILD_OUTPUT: {config_dir}") raise SilentError() from exc config = os.path.join(config_dir, config) makef = os.path.join(config_dir, makef) if os.path.exists(config): if args.no_update: print(f"{config} file exists: no modifications have been done") return 0 # else we make a fresh config custom_conf = [] if args.custom: for conf_chunk in args.custom: with open(conf_chunk, encoding="utf-8") as fd: custom_conf += fd.readlines() if args.verbose: print(f"custom:\n{custom_conf}") mod_conf = [] if args.configitem: mod_conf += ["##: final config-item mods"] for conf_item in args.configitem: if not conf_item.startswith("CONFIG_"): conf_item = "CONFIG_" + conf_item mod_conf += [conf_item] if args.verbose: print(f"mods:\n{mod_conf}") conf = ( _GENERIC_CONFIG_OPTIONAL + ["##: Arch-specific options"] + arch.config_base() + custom_conf + mod_conf + _GENERIC_CONFIG ) if args.verbose: print(f"conf:\n{conf}") linuxname = shlex.quote(arch.linuxname) archargs = [f"ARCH={linuxname}"] cross_compile_prefix = f"{arch.gccname}-linux-gnu-" if args.cross_compile != "": cross_compile_prefix = args.cross_compile if not is_native and shutil.which(f"{cross_compile_prefix}-gcc"): gccname = shlex.quote(f"{cross_compile_prefix}-gcc") archargs.append(f"CROSS_COMPILE={gccname}") for var in args.envs: archargs.append(shlex.quote(var)) maketarget: Optional[str] updatetarget = "" if args.allnoconfig: maketarget = "allnoconfig" updatetarget = "syncconfig" elif args.defconfig: maketarget = arch.defconfig_target updatetarget = "olddefconfig" elif args.update: if args.no_update: arg_fail("--update and --no-update cannot be used together") maketarget = None updatetarget = "olddefconfig" else: arg_fail("No mode selected") if not os.path.exists(config): if args.update: print(f"Error: {config} file is missing") return 1 if maketarget is not None: make_args = [] if not os.path.exists(makef): if args.verbose: sys.stderr.write(f"missing {makef}, adding -f $src/Makefile\n") if os.path.exists("Makefile"): # assuming we're in linux srcdir make_args = ["-f", str(os.path.abspath("Makefile"))] if args.verbose: sys.stderr.write(f"adding make_args: {make_args}\n") try: subprocess.check_call(["make"] + make_args + archargs + [maketarget]) except Exception as exc: raise SilentError() from exc # Append virtme configs if args.verbose: sys.stderr.write(f"appending to config: {config}\n") with open(config, "ab") as conffile: conffile.write("\n".join(conf).encode("utf-8")) # Run the update target try: subprocess.check_call(["make"] + archargs + [updatetarget]) except Exception as exc: raise SilentError() from exc make_args = " ".join(archargs) cpu_count = multiprocessing.cpu_count() print(f"Configured. Build with 'make {make_args} -j{cpu_count}'") return 0 def main() -> int: try: return do_it() except SilentError: return 1 if __name__ == "__main__": try: sys.exit(main()) except SilentError: sys.exit(1) virtme-ng-1.35/virtme/commands/mkinitramfs.py000077500000000000000000000032511500572323100213610ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- mode: python -*- # virtme-mkinitramfs: Generate an initramfs image for virtme # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import argparse import sys from .. import mkinitramfs, modfinder, virtmods def make_parser(): parser = argparse.ArgumentParser( description="Generate an initramfs image for virtme", ) parser.add_argument( "--mod-kversion", action="store", default=None, help="Find kernel modules related to kernel version set", ) parser.add_argument( "--rw", action="store_true", default=False, help="Mount initramfs as rw. Default is ro", ) parser.add_argument( "--outfile", action="store", default=None, help="Filename of the resulting initramfs file. Default: send initramfs to stdout", ) return parser def main(): args = make_parser().parse_args() config = mkinitramfs.Config() if args.mod_kversion is not None: config.modfiles = modfinder.find_modules_from_install( virtmods.MODALIASES, kver=args.mod_kversion ) # search for busybox in the root filesystem config.busybox = mkinitramfs.find_busybox(root="/", is_native=True) if args.rw: config.access = "rw" with ( sys.stdout.buffer if args.outfile is None else open(args.outfile, "w+b") ) as buf: mkinitramfs.mkinitramfs(buf, config) return 0 if __name__ == "__main__": sys.exit(main()) virtme-ng-1.35/virtme/commands/run.py000066400000000000000000002012551500572323100176420ustar00rootroot00000000000000# -*- mode: python -*- # virtme-run: The main command-line virtme frontend # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import argparse import atexit import errno import fcntl import itertools import os import platform import re import shlex import signal import subprocess import sys import tempfile import termios from base64 import b64encode from shutil import which from time import sleep from typing import Any, Dict, List, NoReturn, Optional, Tuple from virtme_ng.utils import ( DEFAULT_VIRTME_SSH_HOSTNAME_CID_SEPARATOR, SSH_CONF_FILE, SSH_DIR, VIRTME_SSH_DESTINATION_NAME, VIRTME_SSH_HOSTNAME_CID_SEPARATORS, ) from .. import architectures, mkinitramfs, modfinder, qemu_helpers, resources, virtmods from ..util import SilentError, find_binary_or_raise, get_username def make_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Virtualize your system (or another) under a kernel image", ) g: Any g = parser.add_argument_group( title="Selection of kernel and modules" ).add_mutually_exclusive_group() g.add_argument( "--installed-kernel", action="store", nargs="?", const=platform.release(), default=None, metavar="VERSION", help="[Deprecated] use --kimg instead.", ) g.add_argument( "--kimg", action="store", nargs="?", const=platform.release(), default=None, help="Use specified kernel image or an installed kernel version. " + "If no argument is specified the running kernel will be used.", ) g.add_argument( "--kdir", action="store", metavar="KDIR", help="Use a compiled kernel source directory", ) g = parser.add_argument_group(title="Kernel options") g.add_argument( "--mods", action="store", choices=["none", "use", "auto"], required=True, help="Setup loadable kernel modules inside a compiled kernel source directory " + "(used in conjunction with --kdir); " + "none: ignore kernel modules, use: asks user to refresh virtme's kernel modules directory, " + "auto: automatically refreshes virtme's kernel modules directory", ) g.add_argument( "-a", "--kopt", action="append", default=[], help="Add a kernel option. You can specify this more than once.", ) g = parser.add_argument_group(title="Common guest options") g.add_argument( "--root", action="store", default="/", help="Local path to use as guest root" ) g.add_argument( "--rw", action="store_true", help="Give the guest read-write access to its root filesystem", ) g.add_argument( "--gdb", action="store_true", help="Attach a gdb session to a running instance started with --debug", ) g.add_argument( "--graphics", action="store", nargs="?", metavar="BINARY", const="", help="Show graphical output instead of using a console. " + "An argument can be optionally specified to start a graphical application.", ) g.add_argument( "--verbose", action="store_true", help="Increase console output verbosity." ) g.add_argument( "--net", action="append", const="user", nargs="?", help="Enable basic network access: user, bridge(=
), loop.", ) g.add_argument( "--net-mac-address", action="store", default=None, help="The MAC address to assign to the NIC interface, e.g. 52:54:00:12:34:56. " + "The last octet will be incremented for the next network devices.", ) g.add_argument( "--balloon", action="store_true", help="Allow the host to ask the guest to release memory.", ) g.add_argument( "--sound", action="store_true", help="Enable audio device (if the architecture supports it).", ) g.add_argument( "--vmcoreinfo", action="store_true", help="Enable vmcoreinfo device (if the architecture supports it).", ) g.add_argument( "--snaps", action="store_true", help="Allow to execute snaps inside virtme-ng" ) g.add_argument( "--disk", action="append", default=[], metavar="NAME=PATH", help="Add a read/write virtio-scsi disk. The device node will be /dev/disk/by-id/scsi-0virtme_disk_NAME.", ) g.add_argument( "--blk-disk", action="append", default=[], metavar="NAME=PATH", help="Add a read/write virtio-blk disk. The device nodes will be /dev/disk/by-id/virtio-virtme_disk_blk_NAME.", ) g.add_argument( "--memory", action="store", default=None, help="Set guest memory and qemu -m flag.", ) g.add_argument( "--numa", action="append", default=None, help="Create NUMA nodes in the guest.", ) g.add_argument( "--numa-distance", action="append", default=None, help="Define a distance between two NUMA nodes in the guest (src=ID,dst=ID,val=NUM).", ) g.add_argument( "--cpus", action="store", default=None, help="Set guest cpu and qemu -smp flag." ) g.add_argument( "--name", action="store", default=None, help="Set guest hostname and qemu -name flag.", ) g.add_argument("--user", action="store", help="Change guest user") g = parser.add_argument_group( title="Scripting", description="Using any of the scripting options will run a script in the guest. " + "The script's stdin will be attached to virtme-run's stdin and " + "the script's stdout and stderr will both be attached to virtme-run's stdout. " + "Kernel logs will go to stderr. This behaves oddly if stdin is a terminal; " + "try using 'cat |virtme-run' if you have trouble with script mode.", ) g.add_argument( "--script-sh", action="store", metavar="SHELL_COMMAND", help="Run a one-line shell script in the guest.", ) g.add_argument( "--script-exec", action="store", metavar="BINARY", help="[Deprecated] use --script-sh instead.", ) g = parser.add_argument_group( title="Architecture", description="Options related to architecture selection" ) g.add_argument( "--arch", action="store", metavar="ARCHITECTURE", default=platform.machine(), help="Guest architecture", ) g.add_argument( "--cross-compile", action="store", metavar="CROSS_COMPILE_PREFIX", help="Cross-compile compiler prefix", ) g.add_argument( "--busybox", action="store", metavar="PATH_TO_BUSYBOX", help="Use the specified busybox binary.", ) g = parser.add_argument_group(title="Virtualizer settings") g.add_argument( "--qemu-bin", action="store", default=None, help="Use specified QEMU binary." ) g.add_argument( "-q", "--qemu-opt", action="append", default=[], help="Add a single QEMU argument. Use this when --qemu-opts's greedy behavior is problematic.'", ) g.add_argument( "--qemu-opts", action="store", nargs=argparse.REMAINDER, metavar="OPTS...", help="Additional arguments for QEMU. " + "This will consume all remaining arguments, so it must be specified last. " + "Avoid using -append; use --kopt instead.", ) g = parser.add_argument_group(title="Debugging/testing") g.add_argument( "--disable-microvm", action="store_true", help='Avoid using the "microvm" QEMU architecture (only on x86_64)', ) g.add_argument( "--disable-kvm", action="store_true", help="Avoid using hardware virtualization / KVM", ) g.add_argument( "--force-initramfs", action="store_true", help="Use an initramfs even if unnecessary", ) g.add_argument( "--force-9p", action="store_true", help="Use legacy 9p filesystem as rootfs" ) g.add_argument( "--dry-run", action="store_true", help="Initialize everything but don't run the guest", ) g.add_argument( "--show-command", action="store_true", help="Show the VM command line" ) g.add_argument( "--save-initramfs", action="store", help="Save the generated initramfs to the specified path", ) g.add_argument( "--show-boot-console", action="store_true", help="Show the boot console when running scripts", ) g.add_argument( "--no-virtme-ng-init", action="store_true", help="Fallback to the bash virtme-init (useful for debugging/development)", ) g.add_argument( "--disable-monitor", action="store_true", help="Disable QEMU STDIO monitor" ) g = parser.add_argument_group( title="Guest userspace configuration" ).add_mutually_exclusive_group() g.add_argument( "--pwd", action="store_true", help="Propagate current working directory to the guest", ) g.add_argument("--cwd", action="store", help="Change guest working directory") g = parser.add_argument_group(title="Sharing resources with guest") g.add_argument( "--rwdir", action="append", default=[], help="Supply a read/write directory to the guest. Use --rwdir=path or --rwdir=guestpath=hostpath.", ) g.add_argument( "--rodir", action="append", default=[], help="Supply a read-only directory to the guest. Use --rodir=path or --rodir=guestpath=hostpath.", ) g.add_argument( "--overlay-rwdir", action="append", default=[], help="Supply a directory that is r/w to the guest but read-only in the host. Use --overlay-rwdir=path.", ) g.add_argument( "--nvgpu", action="store", default=None, help="Set guest NVIDIA GPU." ) g = parser.add_argument_group(title="Remote Console") cli_srv_choices = ["console", "ssh"] g.add_argument( "--server", action="store", const=cli_srv_choices[0], nargs="?", choices=cli_srv_choices, help="Enable a server to communicate later from the host to the device using '--client'. " + "By default, a simple console will be offered using a VSOCK connection, and 'socat' for the proxy.", ) g.add_argument( "--client", action="store", const=cli_srv_choices[0], nargs="?", choices=cli_srv_choices, help="Connect to a VM launched with the '--server' option for a remote control.", ) g.add_argument( "--port", action="store", type=int, default=2222, help="Unique port to communicate with a VM.", ) g.add_argument( "--remote-cmd", action="store", metavar="COMMAND", help="To start in the VM a different command than the default one (--server), " + "or to launch this command instead of a prompt (--client).", ) g.add_argument( "--ssh-tcp", action="store_true", help="Use TCP for the SSH connection to the guest", ) return parser _ARGPARSER = make_parser() def arg_fail(message, show_usage=False) -> NoReturn: sys.stderr.write(message + "\n") if show_usage: _ARGPARSER.print_usage() sys.exit(1) def is_file_more_recent(a, b) -> bool: return os.stat(a).st_mtime > os.stat(b).st_mtime def has_memory_suffix(string): pattern = r"\d+[MGK]$" return re.match(pattern, string) is not None class Kernel: __slots__ = [ "kimg", "version", "dtb", "modfiles", "moddir", "use_root_mods", "config", ] kimg: str version: str dtb: Optional[str] modfiles: List[str] moddir: Optional[str] use_root_mods: bool config: Optional[Dict[str, str]] def load_config(self, kdir: str) -> None: cfgfile = os.path.join(kdir, ".config") if os.path.isfile(cfgfile): self.config = {} regex = re.compile("^(CONFIG_[A-Z0-9_]+)=([ymn])$") with open(cfgfile, encoding="utf-8") as fd: for line in fd: m = regex.match(line.strip()) if m: self.config[m.group(1)] = m.group(2) def get_rootfs_from_kernel_path(path): while path and path != "/" and not os.path.exists(path + "/lib/modules"): path, _ = os.path.split(path) # If a distro, like openSUSE Tumbleweed, has /lib symlinked to /usr/lib, # the rootfs may be mistakenly identified as /usr. In such cases, ensure to # get the rootfs from one level higher. if path.endswith("/usr"): path, _ = os.path.split(path) return os.path.abspath(path) def get_kernel_version(path, img_name: Optional[str] = None): if not os.path.exists(path): arg_fail(f"kernel file {path} does not exist, try --build to build the kernel") if not os.access(path, os.R_OK): arg_fail(f"unable to access {path} (check for read permissions)") version_pattern = r"\S{3,}" try: result = subprocess.run( ["file", path], capture_output=True, text=True, check=False ) for item in result.stdout.split(", "): match = re.search(rf"^[vV]ersion ({version_pattern})", item) if match: kernel_version = match.group(1) return kernel_version except FileNotFoundError: sys.stderr.write( "warning: `file` is not installed in the system, " "virtme-ng may fail to detect kernel version\n" ) # 'file' failed to get kernel version, try with 'strings'. result = subprocess.run( ["strings", path], capture_output=True, text=True, check=False ) match = re.search(rf"Linux version ({version_pattern})", result.stdout) if match: kernel_version = match.group(1) return kernel_version # The version detection fails s390x using file or strings tools, so check # if the file itself contains the version number. if img_name is not None: match = re.search(rf"{img_name}-({version_pattern})", path) if match: return match.group(1) match = re.search(rf"/lib/modules/({version_pattern})/{img_name}", path) if match: return match.group(1) return None def find_kernel_and_mods(arch, args) -> Kernel: kernel = Kernel() kernel.config = None kernel.use_root_mods = False if args.installed_kernel is not None: sys.stderr.write( "Warning: --installed-kernel is deprecated. Use --kimg instead.\n" ) args.kimg = args.installed_kernel if args.kimg is not None: # If a locally built kernel image / dir is provided just fallback to # the --kdir case. kdir = None if os.path.exists(args.kimg): if os.path.isdir(args.kimg): kdir = args.kimg elif args.kimg.endswith(arch.kimg_path()): if args.kimg == arch.kimg_path(): kdir = "." else: kdir = args.kimg.split(arch.kimg_path())[0] if kdir is not None and os.path.exists(kdir + "/.config"): args.kdir = kdir args.kimg = None if args.kimg is not None: for img_name in arch.img_name(): # Try to resolve kimg as a kernel version first, then check if a file # is provided. kimg = f"/usr/lib/modules/{args.kimg}/{img_name}" if os.path.exists(kimg): break kimg = f"/boot/{img_name}-{args.kimg}" if os.path.exists(kimg): break else: kimg = args.kimg if not os.path.exists(kimg): arg_fail(f"{args.kimg} does not exist") # The for loop is a workaround for s390x to detect the version number # from the filename. for img_name in arch.img_name(): kver = get_kernel_version(kimg, img_name) if kver is not None: break else: # Unable to detect kernel version, try to boot without # automatically detecting modules. kver = None args.mods = "none" sys.stderr.write( "warning: failed to retrieve kernel version from: " + kimg + " (modules may not work)\n" ) kernel.version = kver kernel.kimg = kimg if args.mods == "none": kernel.modfiles = [] kernel.moddir = None else: # Try to automatically detect modules' path root_dir = get_rootfs_from_kernel_path(kernel.kimg) # If we are using the entire host filesystem or if we are using # a chroot (via --root) we don't have to do anything special action # the modules, just rely on /lib/modules in the target rootfs. if root_dir == "/" or args.root != "/": kernel.use_root_mods = True kernel.moddir = f"{root_dir}/lib/modules/{kver}" if not os.path.exists(kernel.moddir): kernel.modfiles = [] kernel.moddir = None else: mod_file = os.path.join(kernel.moddir, "modules.dep") if not os.path.exists(mod_file): depmod = find_binary_or_raise(["depmod"]) # Try to refresh modules directory. Some packages (e.g., debs) # don't ship all the required modules information, so we # need to refresh the modules directory using depmod. subprocess.call( [depmod, "-a", "-b", root_dir, kver], stderr=subprocess.DEVNULL, ) kernel.modfiles = modfinder.find_modules_from_install( virtmods.MODALIASES, root=root_dir, kver=kver ) kernel.dtb = None # For now elif args.kdir is not None: kimg = os.path.join(args.kdir, arch.kimg_path()) # Run get_kernel_version to check at least if the kernel image exist. kernel.version = get_kernel_version(kimg) kernel.kimg = kimg virtme_mods = os.path.join(args.kdir, ".virtme_mods") mod_file = os.path.join(args.kdir, "modules.order") virtme_mod_file = os.path.join(virtme_mods, "lib/modules/0.0.0/modules.dep") kernel.load_config(args.kdir) # Kernel modules support kver = None kernel.moddir = None kernel.modfiles = [] modmode = args.mods if ( kernel.config is not None and kernel.config.get("CONFIG_MODULES", "n") != "y" ): modmode = "none" if modmode == "none": pass elif modmode in ("use", "auto"): if args.root != "/": kernel.use_root_mods = True kernel.moddir = f"{args.root}/usr/lib/modules/{kernel.version}" if not os.path.exists(kernel.moddir): kernel.moddir = f"{args.root}/lib/modules/{kernel.version}" if not os.path.exists(kernel.moddir): kernel.modfiles = [] kernel.moddir = None # Check if modules.order exists, otherwise fallback to mods=none elif os.path.exists(mod_file): # Check if virtme's kernel modules directory needs to be updated if not os.path.exists(virtme_mod_file) or is_file_more_recent( mod_file, virtme_mod_file ): if modmode == "use": # Inform user to manually refresh virtme's kernel modules # directory arg_fail( "run virtme-prep-kdir-mods to update virtme's kernel modules directory or use --mods=auto" ) else: # Auto-refresh virtme's kernel modules directory try: resources.run_script("virtme-prep-kdir-mods", cwd=args.kdir) except subprocess.CalledProcessError as exc: raise SilentError() from exc kernel.moddir = os.path.join(virtme_mods, "lib/modules", "0.0.0") kernel.modfiles = modfinder.find_modules_from_install( virtmods.MODALIASES, root=virtme_mods, kver="0.0.0" ) else: sys.stderr.write( f"\n{mod_file} not found: kernel modules not enabled or kernel not compiled properly, " + "kernel modules disabled\n\n" ) else: arg_fail(f"invalid argument '{args.mods}', please use --mods=none|use|auto") dtb_path = arch.dtb_path() if dtb_path is None: kernel.dtb = None else: kernel.dtb = os.path.join(args.kdir, dtb_path) else: arg_fail("You must specify a kernel to use.") return kernel class VirtioFS: def __init__(self, guest_tools_path): self.sock = None self.pid = None self.guest_tools_path = guest_tools_path def _cleanup_virtiofs_temp_files(self): # Make sure to kill virtiofsd instances that are still potentially running if self.pid is not None: try: with open(self.pid, encoding="utf-8") as fd: pid = int(fd.read().strip()) os.kill(pid, signal.SIGTERM) except (FileNotFoundError, ValueError, OSError): pass # Clean up temp files temp_files = [self.sock, self.pid] for file_path in temp_files: try: os.remove(file_path) except OSError: pass def _get_virtiofsd_path(self): # Define the possible virtiofsd paths. # # NOTE: do not use the C implementation of qemu's virtiofsd, because it # doesn't support unprivileged-mode execution and it would be totally # unsafe to export the whole rootfs of the host running as root. # # Instead, always rely on the Rust implementation of virtio-fs: # https://gitlab.com/virtio-fs/virtiofsd # # This project is receiving the most attention for new feature development # and the daemon is able to export the entire root filesystem of the host # as non-privileged user. # # Starting with version 8.0, qemu will not ship the C implementation of # virtiofsd anymore, allowing to use the Rust daemon installed in the the # same path (/usr/lib/qemu/virtiofsd), so also consider this one in the # list of possible paths. # # We can detect if the qemu implementation is installed in /usr/lib/qemu, # simply by running the command with --version as non-root. If it returns # an error it means that we are using the qemu daemon and we just skip it. possible_paths = ( f"{self.guest_tools_path}/bin/virtiofsd", which("virtiofsd"), "/usr/libexec/virtiofsd", "/usr/lib/virtiofsd/virtiofsd", "/usr/lib/virtiofsd", "/usr/lib/qemu/virtiofsd", ) for path in possible_paths: if path and os.path.exists(path) and os.access(path, os.X_OK): try: subprocess.check_call( [path, "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) return path except subprocess.CalledProcessError: pass return None def start(self, path, verbose=True): virtiofsd_path = self._get_virtiofsd_path() if virtiofsd_path is None: return False # virtiofsd located, try to start the daemon as non-privileged user. _, self.sock = tempfile.mkstemp(prefix="virtme") self.pid = self.sock + ".pid" # Make sure to clean up temp files before starting the daemon and at exit. self._cleanup_virtiofs_temp_files() atexit.register(self._cleanup_virtiofs_temp_files) # Export the whole root fs of the host, do not enable sandbox, otherwise we # would get permission errors. if not verbose: stderr = "2>/dev/null" else: stderr = "" os.system( f"{virtiofsd_path} --syslog --no-announce-submounts " + f"--socket-path {self.sock} --shared-dir {path} --sandbox none {stderr} &" ) max_attempts = 5 check_duration = 0.1 for _ in range(max_attempts): if os.path.exists(self.pid): break if verbose: sys.stderr.write("virtme: waiting for virtiofsd to start\n") sleep(check_duration) check_duration *= 2 else: if verbose: sys.stderr.write( "virtme-run: failed to start virtiofsd, fallback to 9p" ) return False return True class VirtioFSConfig: def __init__(self, path: str, mount_tag: str, guest_tools_path=None, memory=None): self.path = path self.mount_tag = mount_tag self.guest_tools_path = guest_tools_path self.memory = memory def export_virtiofs( arch: architectures.Arch, qemuargs: List[str], config: VirtioFSConfig, verbose=False, ) -> bool: if not arch.virtiofs_support(): return False # Try to start virtiofsd daemon virtio_fs = VirtioFS(config.guest_tools_path) ret = virtio_fs.start(config.path, verbose) if not ret: return False # Adjust qemu options to use virtiofsd fsid = f"virtfs{len(qemuargs)}" vhost_dev_type = arch.vhost_dev_type("user-fs") qemuargs.extend(["-chardev", f"socket,id=char{fsid},path={virtio_fs.sock}"]) qemuargs.extend( ["-device", f"{vhost_dev_type},chardev=char{fsid},tag={config.mount_tag}"] ) memory = config.memory if config.memory is not None else "128M" if memory == 0: return True qemuargs.extend(["-object", f"memory-backend-memfd,id=mem,size={memory},share=on"]) if arch.numa_support(): qemuargs.extend(["-numa", "node,memdev=mem"]) else: qemuargs.extend(["-machine", "memory-backend=mem"]) return True class VirtFSConfig: def __init__(self, path: str, mount_tag: str, security_model="none", readonly=True): self.path = path self.mount_tag = mount_tag self.security_model = security_model self.readonly = readonly def export_virtfs( qemu: qemu_helpers.Qemu, arch: architectures.Arch, qemuargs: List[str], config: VirtFSConfig, ) -> None: # NB: We can't use -virtfs for this, because it can't handle a mount_tag # that isn't a valid QEMU identifier. fsid = f"virtfs{len(qemuargs)}" qemuargs.extend( [ "-fsdev", "local,id={},path={},security_model={}{}{}".format( fsid, qemu.quote_optarg(config.path), config.security_model, ",readonly=on" if config.readonly else "", ",multidevs=remap" if qemu.has_multidevs else "", ), ] ) qemuargs.extend( [ "-device", "{},fsdev={},mount_tag={}".format( arch.virtio_dev_type("9p"), fsid, qemu.quote_optarg(config.mount_tag) ), ] ) def quote_karg(arg: str) -> str: if '"' in arg: raise ValueError("cannot quote '\"' in kernel args") if " " in arg: return f'"{arg}"' return arg # Validate name=path arguments from --disk and --blk-disk def sanitize_disk_args(func: str, arg: str) -> Tuple[str, str]: namefile = arg.split("=", 1) if len(namefile) != 2: arg_fail(f"invalid argument to {func}") name, fn = namefile if "=" in fn or "," in fn: arg_fail(f"{func} filenames cannot contain '=' or ','") if "=" in name or "," in name: arg_fail(f"{func} device names cannot contain '=' or ','") return name, fn def can_access_file(path): if not os.path.exists(path): return False try: fd = os.open(path, os.O_RDWR | os.O_CLOEXEC) os.close(fd) return True except OSError: return False def can_use_kvm(args): if args.disable_kvm: return False return can_access_file("/dev/kvm") def all_tools_available(tools: List[str]) -> bool: return all(map(lambda tool: which(tool) is not None, tools)) def can_use_ssh_over_vsock(ssh_tcp: bool) -> bool: return ( not ssh_tcp and all_tools_available(["setsid", "systemd-socket-activate"]) and can_access_file("/dev/vhost-vsock") ) def can_use_microvm(args): return ( not args.disable_microvm and not args.numa and args.arch == "x86_64" and can_use_kvm(args) ) def has_read_acl(username, file_path): try: # Execute the `getfacl` command and capture the output output = subprocess.check_output( ["getfacl", file_path], stderr=subprocess.DEVNULL ).decode("utf-8") # Parse the output to check for the current user's read permission lines = output.split("\n") for line in lines: if line.startswith(f"user:{username}"): parts = line.split(":") if len(parts) >= 3 and "r" in parts[2]: return True # Current user has read permission return False # Current user does not have read permission except subprocess.CalledProcessError: return False # Error occurred while executing getfacl command def is_statically_linked(binary_path): try: # Run the 'file' command on the binary and check for the string # "statically linked" result = subprocess.check_output(["file", binary_path], universal_newlines=True) return "statically linked" in result except subprocess.CalledProcessError: return False def is_subpath(path, potential_parent): # Normalize the paths to avoid issues with trailing slashes and different formats path = os.path.abspath(path) potential_parent = os.path.abspath(potential_parent) # Find the common path common_path = os.path.commonpath([path, potential_parent]) # Check if the common path is the same as the potential parent path return common_path == potential_parent def get_console_path(port): return os.path.join(tempfile.gettempdir(), "virtme-console", f"{port}.sh") def console_client(args): if which("socat") is None: arg_fail("socat tool is required, but not available") try: # with tty support (cols, rows) = os.get_terminal_size() stty = f"stty rows {rows} cols {cols} iutf8 echo" socat_in = f"file:{os.ttyname(sys.stdin.fileno())},raw,echo=0" except OSError: stty = "" socat_in = "-" socat_out = f"VSOCK-CONNECT:{args.port}:1024" user = args.user if args.user else "${virtme_user:-root}" if args.pwd: cwd = os.path.relpath(os.getcwd(), args.root) elif args.cwd is not None: cwd = os.path.relpath(args.cwd, args.root) else: cwd = '${virtme_chdir:+"${virtme_chdir}"}' # use 'su' only if needed: another use, or to get a prompt cmd = f'if [ "{user}" != "root" ]; then\n' + f' exec su "{user}"' if args.remote_cmd is not None: exec_escaped = args.remote_cmd.replace('"', '\\"') cmd += f' -c "{exec_escaped}"' + "\nelse\n" + f" {args.remote_cmd}\n" else: cmd += "\nelse\n" + " exec su\n" cmd += "fi" console_script_path = get_console_path(args.port) with open(console_script_path, "w", encoding="utf-8") as file: print( ( "#! /bin/bash\n" "main() {\n" f"{stty}\n" f'HOME=$(getent passwd "{user}" | cut -d: -f6)\n' f"cd {cwd}\n" f"{cmd}\n" "}\n" "main" # use a function to avoid issues when the script is modified ), file=file, ) os.chmod(console_script_path, 0o755) if args.dry_run: print("socat", socat_in, socat_out) else: os.execvp("socat", ["socat", socat_in, socat_out]) def console_server(args, qemu, arch, qemuargs, kernelargs): console_script_path = get_console_path(args.port) if os.path.exists(console_script_path): arg_fail( f"console: '{console_script_path}' file exists: " + "another VM is running with the same --port? " + "If not, remove this file." ) def cleanup_console_script(): os.unlink(console_script_path) # create an empty file that can be populated later on console_script_dir = os.path.dirname(console_script_path) os.makedirs(console_script_dir, exist_ok=True) open(console_script_path, "w", encoding="utf-8").close() atexit.register(cleanup_console_script) if args.remote_cmd is not None: console_exec = args.remote_cmd else: console_exec = console_script_path if args.root != "/": virtfs_config = VirtFSConfig( path=console_script_dir, mount_tag="virtme.vsockmount", ) export_virtfs(qemu, arch, qemuargs, virtfs_config) kernelargs.append(f"virtme_vsockmount={console_script_dir}") kernelargs.extend([f"virtme.vsockexec=`{console_exec}`"]) qemuargs.extend( ["-device", f"{arch.vhost_dev_type('vsock')},guest-cid={args.port}"] ) def ssh_client(args): if can_use_ssh_over_vsock(args.ssh_tcp): ssh_destination = f"{VIRTME_SSH_DESTINATION_NAME}{DEFAULT_VIRTME_SSH_HOSTNAME_CID_SEPARATOR}{args.port}" else: ssh_destination = f"ssh://{VIRTME_SSH_DESTINATION_NAME}:{args.port}" if args.remote_cmd is not None: exec_escaped = shlex.quote(args.remote_cmd) remote_cmd = ["--", "bash", "-c", exec_escaped] else: remote_cmd = [] cmd = ["ssh", "-F", f"{SSH_CONF_FILE}"] if args.user: cmd += ["-l", f"{args.user}"] cmd += [ssh_destination] + remote_cmd if args.dry_run: print(shlex.join(cmd)) else: os.execvp("ssh", cmd) def ssh_server(args, arch, qemuargs, kernelargs): # Check if we need to generate the SSH host keys for the guest. SSH_ETC_SSH_DIR = SSH_DIR.joinpath("etc", "ssh") SSH_ETC_SSH_DIR.mkdir(mode=0o755, parents=True, exist_ok=True) subprocess.check_call(["ssh-keygen", "-A", "-f", f"{SSH_DIR}"]) # Tell virtme-ng-init / virtme-init to start sshd and use the current # username keys/credentials. username = get_username() if can_use_ssh_over_vsock(args.ssh_tcp): qemuargs.extend( [ "-device", f"{arch.vhost_dev_type('vsock')},guest-cid={args.port}", ] ) ssh_channel_type = "vsock" else: # Implicitly enable dhcp to automatically get an IP on the network # interface and prevent interface renaming. kernelargs.extend(["virtme.dhcp", "net.ifnames=0", "biosdevname=0"]) # Setup a port forward network interface for the guest. qemuargs.extend(["-device", f"{arch.virtio_dev_type('net')},netdev=ssh"]) qemuargs.extend( ["-netdev", f"user,id=ssh,hostfwd=tcp:127.0.0.1:{args.port}-:22"] ) ssh_channel_type = "tcp" kernelargs.extend( [ "virtme.ssh", f"virtme_ssh_channel={ssh_channel_type}", f"virtme_ssh_user={username}", ] ) ssh_proxy = os.path.realpath(resources.find_script("virtme-ssh-proxy")) with open(SSH_CONF_FILE, "w", encoding="utf-8") as f: f.write(f"""Host {VIRTME_SSH_DESTINATION_NAME}* CheckHostIP no # Disable all kinds of host identity checks, since these addresses are generally ephemeral. StrictHostKeyChecking no UserKnownHostsFile /dev/null Host {VIRTME_SSH_DESTINATION_NAME} HostName localhost Host""") for sep in VIRTME_SSH_HOSTNAME_CID_SEPARATORS: f.write(f" {VIRTME_SSH_DESTINATION_NAME}{sep}*") f.write(f""" ProxyCommand "{ssh_proxy}" --port %p %h ProxyUseFdpass yes """) # Allowed characters in mount paths. We can extend this over time if needed. _SAFE_PATH_PATTERN = "[a-zA-Z0-9_+ /.-]+" _RWDIR_RE = re.compile(f"^({_SAFE_PATH_PATTERN})(?:=({_SAFE_PATH_PATTERN}))?$") def do_it() -> int: args = _ARGPARSER.parse_args() if args.client is not None: if args.server is not None: arg_fail("--client cannot be used with --server.") if args.client == "console": console_client(args) elif args.client == "ssh": ssh_client(args) sys.exit(0) arch = architectures.get(args.arch) is_native = args.arch == platform.machine() qemu = qemu_helpers.Qemu(args.qemu_bin, arch.qemuname) qemu.probe() # Check if initramfs is required. need_initramfs = args.force_initramfs or qemu.cannot_overmount_virtfs config = mkinitramfs.Config() if len(args.overlay_rwdir) > 0: virtmods.MODALIASES.append("overlay") kernel = find_kernel_and_mods(arch, args) config.modfiles = kernel.modfiles if config.modfiles: need_initramfs = True if args.gdb: if kernel.version: print(f"kernel version = {kernel.version}") vmlinux = "" if os.path.exists("vmlinux"): vmlinux = "vmlinux" elif os.path.exists(f"/usr/lib/debug/boot/vmlinux-{kernel.version}"): vmlinux = f"/usr/lib/debug/boot/vmlinux-{kernel.version}" command = ["gdb", "-q", "-ex", "target remote localhost:1234", vmlinux] os.execvp("gdb", command) sys.exit(0) qemuargs: List[str] = [qemu.qemubin] kernelargs = [] # Put the '-name' flag first so it's easily visible in ps, top, etc. if args.name: qemuargs.extend(["-name", args.name]) kernelargs.append(f"virtme_hostname={args.name}") if args.memory: # If no memory suffix is specified, assume it's MB. if not has_memory_suffix(args.memory): args.memory += "M" qemuargs.extend(["-m", args.memory]) # Propagate /proc/sys/fs/nr_open from the host to the guest, otherwise we # may see some EPERM errors, because certain applications/settings may # expect to be able to use a higher limit of the max number of open files. try: with open("/proc/sys/fs/nr_open", encoding="utf-8") as file: nr_open = file.readline().strip() kernelargs.append(f"nr_open={nr_open}") except FileNotFoundError: pass # Parse NUMA settings. if args.numa: for i, numa in enumerate(args.numa, start=1): size, cpus = numa.split(",", 1) if "," in numa else (numa, None) cpus = f",{cpus}" if cpus else "" qemuargs.extend( [ "-object", f"memory-backend-memfd,id=mem{i},size={size},share=on", "-numa", f"node,memdev=mem{i}{cpus}", ] ) if args.numa_distance: for arg in args.numa_distance: qemuargs.extend(["-numa", f"dist,{arg}"]) if args.snaps: if args.root == "/": snapd_state = "/var/lib/snapd/state.json" if os.path.exists(snapd_state): username = get_username() if not has_read_acl(username, snapd_state): # Warn if snapd requires permission adjustments. cmd = f"sudo setfacl -m u:{username}:r {snapd_state}" sys.stderr.write( f"WARNING: `--snaps` specified but {snapd_state} is not readable.\n" ) sys.stderr.write( f"Running `{cmd}` to enable snaps in the guest.\n\n" ) sys.stderr.write( "❗This may have security implications on the **host** (CTRL+C to abort)❗\n\n" ) try: subprocess.run(cmd.split(" "), check=False) except KeyboardInterrupt: sys.exit(1) if args.verbose: sys.stderr.write("virtme: enable snap support\n") kernelargs.append("virtme.snapd") else: sys.stderr.write( f"\nWARNING: {snapd_state} does not exist, snap support is disabled.\n\n" ) else: sys.stderr.write( "\nWARNING: snaps can be enabled only when exporting the entire rootfs to the guest.\n\n" ) guest_tools_path = resources.find_guest_tools() if guest_tools_path is None: raise ValueError("couldn't find guest tools -- virtme is installed incorrectly") # Try to use virtio-fs first, in case of failure fallback to 9p, unless 9p # is forced. if args.force_9p: use_virtiofs = False else: # Try to switch to 'microvm' on x86_64, but only if virtio-fs can be # used for now. if can_use_microvm(args): virt_arch = architectures.get("microvm") else: virt_arch = arch virtiofs_config = VirtioFSConfig( path=args.root, mount_tag="ROOTFS", guest_tools_path=guest_tools_path, # virtiofsd requires a NUMA not, if --numa is specified simply use # the user-defined NUMA node, otherwise create a NUMA node with all # the memory. memory=0 if args.numa else args.memory, ) use_virtiofs = export_virtiofs( virt_arch, qemuargs, virtiofs_config, verbose=args.verbose, ) if can_use_microvm(args) and use_virtiofs: if args.verbose: sys.stderr.write("virtme: use 'microvm' QEMU architecture\n") arch = virt_arch if not use_virtiofs: virtfs_config = VirtFSConfig( path=args.root, mount_tag="/dev/root", readonly=(not args.rw), ) export_virtfs(qemu, arch, qemuargs, virtfs_config) # Use the faster virtme-ng-init if we are running on a native architecture. if ( is_native and not args.no_virtme_ng_init and os.path.exists(guest_tools_path + "/bin/virtme-ng-init") ): virtme_init_cmd = "bin/virtme-ng-init" else: virtme_init_cmd = "virtme-init" if args.root == "/": initcmds = [f"init={guest_tools_path}/{virtme_init_cmd}"] else: virtfs_config = VirtFSConfig( path=guest_tools_path, mount_tag="virtme.guesttools", ) export_virtfs(qemu, arch, qemuargs, virtfs_config) initcmds = [ "init=/bin/sh", "--", "-c", ";".join( [ "mount -t tmpfs run /run", "mkdir -p /run/virtme/guesttools", "/bin/mount -n -t 9p -o ro,version=9p2000.L,trans=virtio,access=any " + "virtme.guesttools /run/virtme/guesttools", f"exec /run/virtme/guesttools/{virtme_init_cmd}", ] ), ] # Arrange for modules to end up in the right place if kernel.moddir is not None: if kernel.use_root_mods: # Tell virtme-init to use the root /lib/modules kernelargs.append("virtme_root_mods=1") else: # We're grabbing modules from somewhere other than /lib/modules. # Rather than mounting it separately, symlink it in the guest. # This allows symlinks within the module directory to resolve # correctly in the guest. kernelargs.append( f"virtme_link_mods=/{qemu.quote_optarg(os.path.relpath(kernel.moddir, args.root))}" ) else: # No modules are available. virtme-init will hide /lib/modules/KVER pass # Set up mounts mount_index = 0 for dirtype, dirarg in itertools.chain( (("rwdir", i) for i in args.rwdir), (("rodir", i) for i in args.rodir) ): m = _RWDIR_RE.match(dirarg) if not m: arg_fail(f"invalid --{dirtype} parameter {dirarg!r}") if m.group(2) is not None: guestpath = m.group(1) hostpath = m.group(2) else: hostpath = m.group(1) guestpath = os.path.relpath(hostpath, args.root) if guestpath.startswith(".."): arg_fail(f"{hostpath!r} is not inside the root") # Check if paths are accessible both on the host and the guest. if not os.path.exists(hostpath): arg_fail(f"error: cannot access {hostpath} on the host") # Guest path must be defined inside one of the overlays guest_path_ok = False for d in args.overlay_rwdir: if os.path.exists(guestpath) or is_subpath(guestpath, d): guest_path_ok = True break if not guest_path_ok: arg_fail( f"error: cannot initialize {guestpath} inside the guest " + "(path must be defined inside a valid overlay)" ) idx = mount_index mount_index += 1 tag = f"virtme.initmount{idx}" virtfs_config = VirtFSConfig( path=hostpath, mount_tag=tag, readonly=(dirtype != "rwdir"), ) export_virtfs(qemu, arch, qemuargs, virtfs_config) kernelargs.append(f"virtme_initmount{idx}={guestpath}") for i, d in enumerate(args.overlay_rwdir): kernelargs.append(f"virtme_rw_overlay{i}={d}") # Turn on KVM if available kvm_ok = can_use_kvm(args) if is_native: if kvm_ok: qemuargs.extend(["-machine", "accel=kvm:tcg"]) elif platform.system() == "Darwin": qemuargs.extend(["-machine", "accel=hvf"]) # Add architecture-specific options qemuargs.extend(arch.qemuargs(is_native, kvm_ok, args.nvgpu is not None)) # Set up / override baseline devices qemuargs.extend(["-parallel", "none"]) qemuargs.extend(["-net", "none"]) if args.graphics is None and not args.script_sh and not args.script_exec: qemuargs.extend(["-echr", "1"]) # Redirect kernel errors to stderr, creating a separate console. # # If we don't have access to stderr via procfs (for example when # running inside a container), print a warning and implicitly # suppress the kernel errors redirection. if can_access_file("/proc/self/fd/2"): qemuargs.extend(["-chardev", "file,path=/proc/self/fd/2,id=dmesg"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend(["-device", "virtconsole,chardev=dmesg"]) kernelargs.extend(["console=hvc0"]) else: print( "WARNING: unable to write kernel messages, try to run vng with a valid PTS " "(e.g., inside tmux or screen)", file=sys.stderr, ) # Unfortunately we can't use hvc0 to redirect early console # messages to stderr, so just send them to the main console, in # this way we don't lose early printk's in verbose mode and we can # catch potential boot issues. if args.verbose: kernelargs.extend(arch.earlyconsole_args()) qemuargs.extend( [ "-chardev", "stdio,id=console,signal=off,mux={}".format( "off" if args.disable_monitor else "on" ), ] ) qemuargs.extend(["-serial", "chardev:console"]) if not args.disable_monitor: qemuargs.extend(["-mon", "chardev=console"]) kernelargs.extend( ["virtme_console=" + arg for arg in arch.serial_console_args()] ) if args.nvgpu is None: qemuargs.extend(arch.qemu_nodisplay_args()) else: qemuargs.extend(arch.qemu_nodisplay_nvgpu_args()) # PS/2 probing is slow; give the kernel a hint to speed it up. kernelargs.extend(["psmouse.proto=exps"]) # Fix the terminal defaults (and set iutf8 because that's a better # default nowadays). I don't know of any way to keep this up to date # after startup, though. try: terminal_size = os.get_terminal_size() kernelargs.extend( [ f"virtme_stty_con=rows {terminal_size.lines} cols {terminal_size.columns} iutf8" ] ) except OSError as e: # don't die if running with a non-TTY stdout if e.errno != errno.ENOTTY: raise # Propagate the terminal type if "TERM" in os.environ: kernelargs.extend(["TERM={}".format(os.environ["TERM"])]) if args.sound: qemuargs.extend(arch.qemu_sound_args()) kernelargs.extend(["virtme.sound"]) if args.balloon: qemuargs.extend( ["-device", "{},id=balloon0".format(arch.virtio_dev_type("balloon"))] ) if args.cpus: qemuargs.extend(["-smp", args.cpus]) if args.blk_disk: for i, d in enumerate(args.blk_disk): driveid = f"blk-disk{i}" name, fn = sanitize_disk_args("--blk-disk", d) qemuargs.extend( [ "-drive", f"if=none,id={driveid},file={fn}", "-device", "{},drive={},serial={}".format( arch.virtio_dev_type("blk"), driveid, name ), ] ) if args.disk: qemuargs.extend(["-device", "{},id=scsi".format(arch.virtio_dev_type("scsi"))]) for i, d in enumerate(args.disk): driveid = f"disk{i}" name, fn = sanitize_disk_args("--disk", d) qemuargs.extend( [ "-drive", f"if=none,id={driveid},file={fn}", "-device", f"scsi-hd,drive={driveid},vendor=virtme,product=disk,serial={name}", ] ) ret_path = None def cleanup_script_retcode(): os.unlink(ret_path) def fetch_script_retcode(): if ret_path is None: return None try: with open(ret_path, encoding="utf-8") as file: number_str = file.read().strip() if number_str.isdigit(): return int(number_str) return None except FileNotFoundError: return None def do_script(shellcmd: str, ret_path=None, show_boot_console=False) -> None: if args.graphics is None: # Turn off default I/O if args.nvgpu is None: qemuargs.extend(arch.qemu_nodisplay_args()) else: qemuargs.extend(arch.qemu_nodisplay_nvgpu_args()) # Check if we can redirect stdin/stdout/stderr. if ( not can_access_file("/proc/self/fd/0") or not can_access_file("/proc/self/fd/1") or not can_access_file("/proc/self/fd/2") ): print( "ERROR: not a valid pts, try to run vng with a valid PTS " "(e.g., inside tmux or screen)", file=sys.stderr, ) sys.exit(1) # Configure kernel console output if show_boot_console: output = "/proc/self/fd/2" console_args = () else: output = "/dev/null" console_args = ["quiet", "loglevel=0"] qemuargs.extend(arch.qemu_serial_console_args()) qemuargs.extend(["-chardev", f"file,id=console,path={output}"]) kernelargs.extend(["console=" + arg for arg in arch.serial_console_args()]) kernelargs.extend(arch.earlyconsole_args()) kernelargs.extend(console_args) # Set up a virtserialport for script I/O # # NOTE: we need two additional I/O ports for /dev/stdout and # /dev/stderr in the guest. # # This is needed because virtio serial ports are designed to support a # single writer at a time, so any attempt to write directly to # /dev/stdout or /dev/stderr in the guest will result in an -EBUSY # error. qemuargs.extend(["-chardev", "stdio,id=stdin,signal=on,mux=off"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend(["-device", "virtserialport,name=virtme.stdin,chardev=stdin"]) qemuargs.extend(["-chardev", "file,id=stdout,path=/proc/self/fd/1"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend(["-device", "virtserialport,name=virtme.stdout,chardev=stdout"]) qemuargs.extend(["-chardev", "file,id=stderr,path=/proc/self/fd/2"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend(["-device", "virtserialport,name=virtme.stderr,chardev=stderr"]) qemuargs.extend(["-chardev", "file,id=dev_stdout,path=/proc/self/fd/1"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend( ["-device", "virtserialport,name=virtme.dev_stdout,chardev=dev_stdout"] ) qemuargs.extend(["-chardev", "file,id=dev_stderr,path=/proc/self/fd/2"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend( ["-device", "virtserialport,name=virtme.dev_stderr,chardev=dev_stderr"] ) # Create a virtio serial device to channel the retcode of the script # executed in the guest to the host. if ret_path is not None: qemuargs.extend(["-chardev", f"file,id=ret,path={ret_path}"]) qemuargs.extend(["-device", arch.virtio_dev_type("serial")]) qemuargs.extend(["-device", "virtserialport,name=virtme.ret,chardev=ret"]) # Scripts shouldn't reboot and shouldn't hang on panic: make sure to # force an exit condition if a panic happens. qemuargs.extend(["-no-reboot"]) kernelargs.append("panic=-1") # Nasty issue: QEMU will set O_NONBLOCK on fds 0, 1, and 2. # This isn't inherently bad, but it can cause a problem if # another process is reading from 1 or writing to 0, which is # exactly what happens if you're using a terminal and you # redirect some, but not all, of the tty fds. Work around it # by giving QEMU private copies of the open object if either # of them is a terminal. for oldfd, mode in ((0, os.O_RDONLY), (1, os.O_WRONLY), (2, os.O_WRONLY)): if os.isatty(oldfd): try: newfd = os.open(f"/proc/self/fd/{oldfd}", mode) except OSError: pass else: os.dup2(newfd, oldfd) os.close(newfd) # Encode the shell command to base64 to handle special characters (such # as quotes, double quotes, etc.). shellcmd = b64encode(shellcmd.encode("utf-8")).decode("utf-8") if args.graphics is not None: kernelargs.append("virtme_graphics=1") # Ask virtme-init to run the script kernelargs.append(f"virtme.exec=`{shellcmd}`") # Do not break old syntax "-g command" if args.graphics is not None and args.script_sh is None: args.script_sh = args.graphics if args.script_sh is not None: _, ret_path = tempfile.mkstemp(prefix="virtme_ret") atexit.register(cleanup_script_retcode) do_script( args.script_sh, ret_path=ret_path, show_boot_console=args.show_boot_console ) if args.script_exec is not None: do_script( shlex.quote(args.script_exec), show_boot_console=args.show_boot_console, ) if args.graphics is not None and args.nvgpu is None: video_args = arch.qemu_display_args() if video_args: qemuargs.extend(video_args) def get_net_mac(index): if args.net_mac_address is None: return "" mac = args.net_mac_address.split(":") try: mac[5] = "%02x" % ((int(mac[5], 16) + index) % 256) except (ValueError, IndexError): arg_fail( f"--net-mac-address: invalid MAC address: '{args.net_mac_address}'" ) return ",mac=" + ":".join(mac) if args.net: extend_dhcp = False index = 0 for net in args.net: qemuargs.extend( [ "-device", "{},netdev=n{}{}".format( arch.virtio_dev_type("net"), index, get_net_mac(index) ), ] ) if net == "user": qemuargs.extend(["-netdev", f"user,id=n{index}"]) extend_dhcp = True elif net == "bridge" or net.startswith("bridge="): if len(net) > 7 and net[6] == "=": bridge = net[7:] else: bridge = "virbr0" qemuargs.extend(["-netdev", f"bridge,id=n{index},br={bridge}"]) extend_dhcp = True elif net == "loop": hubid = index qemuargs.extend(["-netdev", f"hubport,id=n{index},hubid={hubid}"]) index += 1 qemuargs.extend( [ "-device", "{},netdev=n{}{}".format( arch.virtio_dev_type("net"), index, get_net_mac(index) ), ] ) qemuargs.extend(["-netdev", f"hubport,id=n{index},hubid={hubid}"]) else: arg_fail( f"--net: invalid choice: '{net}' (choose from user, bridge(=
), loop)" ) index += 1 if extend_dhcp: kernelargs.extend(["virtme.dhcp"]) kernelargs.extend( [ # Prevent annoying interface renaming "net.ifnames=0", "biosdevname=0", ] ) if args.server is not None: if args.server == "console": console_server(args, qemu, arch, qemuargs, kernelargs) elif args.server == "ssh": ssh_server(args, arch, qemuargs, kernelargs) if args.pwd: rel_pwd = os.path.relpath(os.getcwd(), args.root) if rel_pwd.startswith(".."): print("current working directory is not contained in the root") return 1 kernelargs.append(f"virtme_chdir={rel_pwd}") if args.cwd is not None: if args.pwd: arg_fail("--pwd and --cwd are mutually exclusive") rel_cwd = os.path.relpath(args.cwd, args.root) if rel_cwd.startswith(".."): print("specified working directory is not contained in the root") return 1 kernelargs.append(f"virtme_chdir={rel_cwd}") if args.user and args.user != "root": kernelargs.append(f"virtme_user={args.user}") if args.nvgpu: qemuargs.extend(["-device", args.nvgpu]) # If we are running as root on the host pass this information to the guest # (this can be useful to properly support running virtme-ng instances # inside docker) if os.geteuid() == 0: kernelargs.append("virtme_root_user=1") initrdpath: Optional[str] if need_initramfs: if args.busybox is not None: config.busybox = args.busybox else: busybox = mkinitramfs.find_busybox(args.root, is_native) if busybox is None: print( "virtme-run: initramfs is needed, and no busybox was found", file=sys.stderr, ) return 1 if not is_statically_linked(busybox): print( "virtme-run: a statically linked busybox could not be found, " "please install busybox-static", file=sys.stderr, ) return 1 config.busybox = busybox if args.rw: config.access = "rw" # Set up the initramfs (warning: hack ahead) if args.save_initramfs is not None: initramfsfile = open(args.save_initramfs, "xb") else: initramfsfile = tempfile.TemporaryFile(suffix="irfs") initramfsfd = initramfsfile.fileno() mkinitramfs.mkinitramfs(initramfsfile, config) initramfsfile.flush() if args.save_initramfs is not None: initrdpath = args.save_initramfs else: fcntl.fcntl(initramfsfd, fcntl.F_SETFD, 0) initrdpath = f"/proc/self/fd/{initramfsfd}" else: if args.save_initramfs is not None: print( "--save_initramfs specified but initramfs is not used", file=sys.stderr ) return 1 # No initramfs! Warning: this is slower than using an initramfs # because the kernel will wait for device probing to finish. # Sigh. if use_virtiofs: kernelargs.extend( [ "rootfstype=virtiofs", "root=ROOTFS", ] ) else: kernelargs.extend( [ "rootfstype=9p", "rootflags=version=9p2000.L,trans=virtio,access=any", ] ) kernelargs.extend( [ "raid=noautodetect", "rw" if args.rw else "ro", ] ) initrdpath = None if args.verbose: kernelargs.append("debug") else: kernelargs.append("quiet") kernelargs.append("loglevel=1") # Now that we're done setting up kernelargs, append user-specified args # and then initargs kernelargs.extend(args.kopt) # Unknown options get turned into arguments to init, which is annoying # because we're explicitly passing '--' to set the arguments directly. # Fortunately, 'init=' will clear any arguments parsed so far, so make # sure that 'init=' appears directly before '--'. kernelargs.extend(initcmds) # Load a normal kernel qemuargs.extend(["-kernel", kernel.kimg]) if kernelargs: qemuargs.extend(["-append", " ".join(quote_karg(a) for a in kernelargs)]) if initrdpath is not None: qemuargs.extend(["-initrd", initrdpath]) if kernel.dtb is not None: qemuargs.extend(["-dtb", kernel.dtb]) if args.vmcoreinfo is True: qemuargs.extend(arch.qemu_vmcoreinfo_args()) # Handle --qemu-opt(s) qemuargs.extend(args.qemu_opt) if args.qemu_opts is not None: qemuargs.extend(args.qemu_opts) if args.show_command: print(shlex.join(qemuargs)) # Go! if not args.dry_run: pid = os.fork() if pid: try: pid, status = os.waitpid(pid, 0) ret = fetch_script_retcode() if ret is not None: return ret if not args.script_sh and not args.script_exec: return status # Return special error code 255 in case of unexpected exit # (e.g., kernel panic). return 255 except KeyboardInterrupt: sys.stderr.write("Interrupted.") sys.exit(1) else: os.execv(qemu.qemubin, qemuargs) return 0 def save_terminal_settings(): return termios.tcgetattr(sys.stdin) if sys.stdin.isatty() else None def restore_terminal_settings(settings): if settings is not None: termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, settings) def signal_handler(_signum, _frame): sys.exit(1) def main() -> int: # Catch potential signals that may interrupt the execution (SIGTERM) and # make sure the terminal settings are restored on exit. settings = save_terminal_settings() signal.signal(signal.SIGTERM, signal_handler) try: return do_it() except SilentError: return 1 finally: restore_terminal_settings(settings) if __name__ == "__main__": main() virtme-ng-1.35/virtme/cpiowriter.py000066400000000000000000000075771500572323100174370ustar00rootroot00000000000000# -*- mode: python -*- # cpiowriter: A barebones initramfs writer # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 class FileMetaData: def __init__(self, **kwargs): # Define default values for the metadata defaults = { "ino": None, "nlink": None, "uid": 0, "gid": 0, "mtime": 0, "devmajor": 0, "devminor": 0, "rdevmajor": 0, "rdevminor": 0, } # Update defaults with any provided keyword arguments self.meta_data = {**defaults, **kwargs} def get(self, key): return self.meta_data.get(key) def set(self, key, value): self.meta_data[key] = value class CpioWriter: TYPE_DIR = 0o0040000 TYPE_REG = 0o0100000 TYPE_SYMLINK = 0o0120000 TYPE_CHRDEV = 0o0020000 TYPE_MASK = 0o0170000 def __init__(self, f): self.__f = f self.__totalsize = 0 self.__next_ino = 0 def __write(self, data): self.__f.write(data) self.__totalsize += len(data) def write_object(self, name, body, mode, meta_data=None): # Set default metadata if not provided meta_data = meta_data or FileMetaData() # Ensure nlink is set correctly based on mode if meta_data.get("nlink") is None: meta_data.set( "nlink", 2 if (mode & CpioWriter.TYPE_MASK) == CpioWriter.TYPE_DIR else 1, ) if b"\0" in name: raise ValueError("Filename cannot contain a NUL") namesize = len(name) + 1 if isinstance(body, bytes): filesize = len(body) else: filesize = body.seek(0, 2) body.seek(0) # Set default ino if not provided if meta_data.get("ino") is None: meta_data.set("ino", self.__next_ino) self.__next_ino += 1 # Prepare fields list using metadata fields = [ meta_data.get("ino"), mode, meta_data.get("uid"), meta_data.get("gid"), meta_data.get("nlink"), meta_data.get("mtime"), filesize, meta_data.get("devmajor"), meta_data.get("devminor"), meta_data.get("rdevmajor"), meta_data.get("rdevminor"), namesize, 0, ] hdr = ("070701" + "".join(f"{f:08X}" for f in fields)).encode("ascii") self.__write(hdr) self.__write(name) self.__write(b"\0") self.__write(((2 - namesize) % 4) * b"\0") if isinstance(body, bytes): self.__write(body) else: while True: buf = body.read(65536) if buf == b"": break self.__write(buf) self.__write(((-filesize) % 4) * b"\0") def write_trailer(self): self.write_object( name=b"TRAILER!!!", body=b"", mode=0, meta_data=FileMetaData(ino=0, nlink=1) ) self.__write(((-self.__totalsize) % 512) * b"\0") def mkdir(self, name, mode): self.write_object(name=name, body=b"", mode=CpioWriter.TYPE_DIR | mode) def symlink(self, src, dst): self.write_object(name=dst, body=src, mode=CpioWriter.TYPE_SYMLINK | 0o777) def write_file(self, name, body, mode): self.write_object(name=name, body=body, mode=CpioWriter.TYPE_REG | mode) def mkchardev(self, name, dev, mode): major, minor = dev self.write_object( name=name, body=b"", mode=CpioWriter.TYPE_CHRDEV | mode, meta_data=FileMetaData( rdevmajor=major, rdevminor=minor, ), ) virtme-ng-1.35/virtme/guest/000077500000000000000000000000001500572323100160055ustar00rootroot00000000000000virtme-ng-1.35/virtme/guest/__init__.py000066400000000000000000000000001500572323100201040ustar00rootroot00000000000000virtme-ng-1.35/virtme/guest/virtme-init000077500000000000000000000407721500572323100202140ustar00rootroot00000000000000#!/bin/bash # virtme-init: virtme's basic init (PID 1) process # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin log() { if [[ -e /dev/kmsg ]]; then echo "<6>virtme-init: $*" > /dev/kmsg else echo "virtme-init: $*" fi } # Mount procfs and sysfs (needed for stat, sadly) mount -t proc -o nosuid,noexec,nodev proc /proc/ mount -t sysfs -o nosuid,noexec,nodev sys /sys/ # Mount tmpfs dirs mount -t tmpfs -o mode=0755 run /run/ mkdir /run/tmp # Setup rw filesystem overlays for tag in "${!virtme_rw_overlay@}"; do dir="${!tag}" upperdir="/run/tmp/$tag/upper" workdir="/run/tmp/$tag/work" mkdir -p "$upperdir" "$workdir" mnt_opts="lowerdir=$dir,upperdir=$upperdir,workdir=$workdir" mount -t overlay -o xino=off,"${mnt_opts}" "${tag}" "${dir}" || mount -t overlay -o "${mnt_opts}" "${tag}" "${dir}" & done # Setup kernel modules kver="$(uname -r)" # Make sure to always have /lib/modules, otherwise we won't be able to # configure kmod support properly (this can happen in some container # environments, such as docker). if [[ ! -d /lib/modules ]]; then mkdir -p /lib/modules fi if [[ -n $virtme_root_mods ]]; then # /lib/modules is already set up true elif [[ -n $virtme_link_mods ]]; then mount -n -t tmpfs none /lib/modules ln -s "$virtme_link_mods" "/lib/modules/$kver" elif [[ -d "/lib/modules/$kver" ]]; then # We may have mismatched modules. Mask them off. mount -n -t tmpfs -o ro,mode=0000 disallow_modules "/lib/modules/$kver" fi # Adjust max limit of open files if [[ -n ${nr_open} ]]; then printf -- '%s\n' "${nr_open}" > /proc/sys/fs/nr_open fi # devtmpfs might be automounted; if not, mount it. if ! grep -q devtmpfs /proc/mounts; then # Ideally we'll use devtmpfs (but don't rely on /dev/null existing). if [[ -c /dev/null ]]; then mount -n -t devtmpfs -o mode=0755,nosuid,noexec devtmpfs /dev \ &> /dev/null else mount -n -t devtmpfs -o mode=0755,nosuid,noexec devtmpfs /dev fi # shellcheck disable=SC2181 if (($? != 0)); then # The running kernel doesn't have devtmpfs. Use regular tmpfs. mount -t tmpfs -o mode=0755,nosuid,noexec none /dev # Make some basic devices first, and let udev handle the rest mknod -m 0666 /dev/null c 1 3 mknod -m 0660 /dev/kmsg c 1 11 mknod -m 0600 /dev/console c 5 1 fi fi # Setup rw tmpfs directories [ -e /var/log ] && mount -t tmpfs tmpfs /var/log/ [ -e /var/tmp ] && mount -t tmpfs tmpfs /var/tmp/ # Additional rw dirs used by systemd [ -e /var/spool/rsyslog ] && mount -t tmpfs tmpfs /var/spool/rsyslog [ -e /var/lib/portables ] && mount -t tmpfs tmpfs /var/lib/portables [ -e /var/lib/machines ] && mount -t tmpfs tmpfs /var/lib/machines [ -e /var/lib/private ] && mount -t tmpfs tmpfs /var/lib/private [ -e /var/cache ] && mount -t tmpfs tmpfs /var/cache # Additional rw dirs required by apt (if present) [ -e /var/lib/apt ] && mount -t tmpfs tmpfs /var/lib/apt # Additional rw dirs required by snapd (if present) [ -e /var/lib/snapd/cookie ] && mount -t tmpfs tmpfs /var/lib/snapd/cookie # Hide additional sudo settings [ -e /var/lib/sudo ] && mount -t tmpfs tmpfs /var/lib/sudo # Fix up /etc a little bit touch /run/tmp/fstab mount --bind /run/tmp/fstab /etc/fstab if [[ -n $virtme_hostname ]]; then cp /etc/hosts /run/tmp/hosts printf '\n127.0.0.1 %s\n::1 %s\n' "$virtme_hostname" "$virtme_hostname" >> /run/tmp/hosts mount --bind /run/tmp/hosts /etc/hosts fi # Fix dpkg if we are on a Debian-based distro if [ -d /var/lib/dpkg ]; then lock_files=(/var/lib/dpkg/lock /var/lib/dpkg/lock-frontend /var/lib/dpkg/triggers/Lock) for file in "${lock_files[@]}"; do [ -e "$file" ] && touch "/run/tmp/${file##*/}" && mount --bind "/run/tmp/${file##*/}" "$file" done fi # Populate dummy entries in /etc/shadow to allow switching to any user defined # in the system (umask 0644 && touch /run/tmp/shadow) sed -e 's/^\([^:]\+\).*/\1:!:::::::/' < /etc/passwd > /run/tmp/shadow mount --bind /run/tmp/shadow /etc/shadow # The /etc/lvm is usually only read/write by root. In order to allow commands like pvcreate to be # run on rootless users just create a dummy directory and bind mount it in the same place. mkdir /run/tmp/lvm mount --bind /run/tmp/lvm /etc/lvm for tag in "${!virtme_initmount@}"; do if [[ ! -d ${!tag} ]]; then mkdir -p "${!tag}" fi mount -t 9p -o version=9p2000.L,trans=virtio,access=any "virtme.initmount${tag:16}" "${!tag}" || exit 1 done if [[ -n ${virtme_chdir} ]]; then cd -- "${virtme_chdir}" || exit fi log "basic initialization done" ######## The remainder of this script is a very simple init (PID 1) ######## # Does the system use systemd-tmpfiles? if command -v systemd-tmpfiles &> /dev/null; then log "running systemd-tmpfiles" systemd-tmpfiles --create --boot --exclude-prefix="/dev" --exclude-prefix="/root" fi # Make dbus work (if tmpfiles wasn't there or didn't create the directory). install -d /run/dbus # Set up useful things in /sys, assuming our kernel supports it. mount -t configfs configfs /sys/kernel/config &> /dev/null mount -t debugfs debugfs /sys/kernel/debug &> /dev/null mount -t tracefs tracefs /sys/kernel/tracing &> /dev/null mount -t securityfs securityfs /sys/kernel/security &> /dev/null # Set up cgroup mount points (mount cgroupv2 hierarchy by default) # # If SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 is passed we can mimic systemd's # behavior and mount the legacy cgroup v1 layout. if grep -q -E '(^| )SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1($| )' /proc/cmdline; then mount -t tmpfs cgroup /sys/fs/cgroup sybsys=(cpu cpuacct blkio memory devices pids) for s in "${sybsys[@]}"; do mkdir -p "/sys/fs/cgroup/${s}" # Don't treat failure as critical here, since the kernel may not # support all the legacy cgroups. mount -t cgroup "${s}" -o "${s}" "/sys/fs/cgroup/${s}" || true done else mount -t cgroup2 cgroup2 /sys/fs/cgroup fi # Set up filesystems that live in /dev # shellcheck disable=SC2174 # Use -p to ignore errors if the directories already exist mkdir -p -m 0755 /dev /dev/shm /dev/pts mount -t devpts -o gid=tty,mode=620,noexec,nosuid devpts /dev/pts mount -t tmpfs -o mode=1777,nosuid,nodev tmpfs /dev/shm # Find udevd if [[ -x /usr/lib/systemd/systemd-udevd ]]; then udevd=/usr/lib/systemd/systemd-udevd elif [[ -x /lib/systemd/systemd-udevd ]]; then udevd=/lib/systemd/systemd-udevd else udevd=$(command -v udevd) fi # Try to get udevd to coldplug everything. if [[ -n $udevd ]]; then if [[ -e '/sys/kernel/uevent_helper' ]]; then # This kills boot performance. log "you have CONFIG_UEVENT_HELPER on; turn it off" echo '' > /sys/kernel/uevent_helper fi log "starting udevd" udev_out=$($udevd --daemon --resolve-names=never 2>&1) if ! grep -q "quiet" /proc/cmdline; then log "udev: $udev_out" fi log "triggering udev coldplug" udevadm trigger --type=subsystems --action=add > /dev/null 2>&1 udevadm trigger --type=devices --action=add > /dev/null 2>&1 log "waiting for udev to settle" udevadm settle log "udev is done" else log "udevd not found" fi # Install /proc/self/fd symlinks into /dev if not already present declare -r -A fdlinks=( ["/dev/fd"]="/proc/self/fd" ["/dev/stdin"]="/proc/self/fd/0" ["/dev/stdout"]="/proc/self/fd/1" ["/dev/stderr"]="/proc/self/fd/2") for p in "${!fdlinks[@]}"; do [[ -e $p ]] || ln -s "${fdlinks[$p]}" "$p" done if [[ -n $virtme_hostname ]]; then log "Setting hostname to $virtme_hostname..." hostname "$virtme_hostname" fi # Bring up networking ip link set dev lo up # Setup sudoers real_sudoers=/etc/sudoers if [ ! -e ${real_sudoers} ]; then touch ${real_sudoers} fi tmpfile="$(mktemp --tmpdir=/run/tmp)" echo 'Defaults secure_path="/usr/sbin:/usr/bin:/sbin:/bin"' > "$tmpfile" echo "root ALL = (ALL) NOPASSWD: ALL" >> "$tmpfile" if [[ -n ${virtme_user} ]]; then printf -- '%s ALL = (ALL) NOPASSWD: ALL\n' "${virtme_user}" >> "$tmpfile" fi chmod 440 "$tmpfile" if [ ! -f "$real_sudoers" ]; then touch "$real_sudoers" fi mount --bind "$tmpfile" "$real_sudoers" if grep -q -E '(^| )virtme.dhcp($| )' /proc/cmdline; then # Make sure all GIDs are allowed to create raw ICMP sockets (this # allows to run ping as regular user). echo "0 2147483647" > /proc/sys/net/ipv4/ping_group_range # udev is liable to rename the interface out from under us. for d in /sys/bus/virtio/drivers/virtio_net/virtio*/net/*; do virtme_net=$(basename "${d}") busybox udhcpc -i "$virtme_net" -n -q -f -s "$(dirname -- "$0")/virtme-udhcpc-script" & done wait fi if grep -q -E '(^| )virtme.ssh($| )' /proc/cmdline; then "$(dirname -- "$0")"/virtme-sshd-script < /dev/null fi if grep -q -E '(^| )virtme.snapd($| )' /proc/cmdline; then # If snapd is present in the system try to start it, to properly support snaps. snapd_bin="/usr/lib/snapd/snapd" if [ -e "$snapd_bin" ]; then snapd_state="/var/lib/snapd/state.json" if [ -e "$snapd_state" ]; then "$(dirname -- "$0")"/virtme-snapd-script $snapd_bin > /dev/null 2>&1 < /dev/null & snapd_apparmor_bin=/usr/lib/snapd/snapd-apparmor if [ -e $snapd_apparmor_bin ]; then $snapd_apparmor_bin start > /dev/null 2>&1 < /dev/null fi fi fi fi vsock_exec=$(sed -ne "s/.*virtme.vsockexec=\`\(.*\)\`.*/\1/p" /proc/cmdline) if [[ -n ${vsock_exec} ]]; then if [[ -n ${virtme_vsockmount} ]]; then mkdir -p "${virtme_vsockmount}" mount -t 9p -o version=9p2000.L,trans=virtio,access=any "virtme.vsockmount" "${virtme_vsockmount}" fi socat "VSOCK-LISTEN:1024,reuseaddr,fork" \ "EXEC:\"${vsock_exec}\",pty,stderr,setsid,sigint,sane,echo=0" & fi user_cmd=$(sed -ne "s/.*virtme.exec=\`\(.*\)\`.*/\1/p" /proc/cmdline) if [[ -n ${user_cmd} ]]; then if [[ ! -e "/dev/virtio-ports/virtme.stdin" || ! -e "/dev/virtio-ports/virtme.stdout" || ! -e "/dev/virtio-ports/virtme.stderr" || ! -e "/dev/virtio-ports/virtme.dev_stdout" || ! -e "/dev/virtio-ports/virtme.dev_stderr" ]]; then echo "virtme-init: cannot find script I/O ports; make sure virtio-serial is available" poweroff -f exit 1 fi # Set proper ownership on the virtio-ports devices if [[ -n ${virtme_user} ]]; then chown -- "${virtme_user}" \ /dev/virtio-ports/virtme.stdin \ /dev/virtio-ports/virtme.stdout \ /dev/virtio-ports/virtme.stderr \ /dev/virtio-ports/virtme.dev_stdout \ /dev/virtio-ports/virtme.dev_stderr if [ -e /dev/virtio-ports/virtme.ret ]; then chown -- "${virtme_user}" \ /dev/virtio-ports/virtme.ret fi fi # Fix /dev/stdout and /dev/stderr. # # When using a virtio serial port, the EBUSY error can occur if multiple # writers are attempting to access the port simultaneously. The virtio # serial port is designed to support a single writer at a time, which means # that only one process or application can write to the port at any given # moment. # # For this reason create a separate virtio serial port to handle writes # directly to /dev/stdout and /dev/stderr that will be all redirected to # stdout on the host. rm -f /dev/stdout /dev/stderr ln -s /dev/virtio-ports/virtme.dev_stdout /dev/stdout ln -s /dev/virtio-ports/virtme.dev_stderr /dev/stderr # Decode shell command (base64) and dump it to a script printf -- '%s\n' "$user_cmd" | base64 -d > /run/tmp/.virtme-script if [[ -z ${virtme_graphics} ]]; then # Start the script log 'starting script' if [[ -n ${virtme_user} ]]; then chmod +x /run/tmp/.virtme-script setsid su - "${virtme_user}" -c /run/tmp/.virtme-script < /dev/virtio-ports/virtme.stdin > /dev/virtio-ports/virtme.stdout 2> /dev/virtio-ports/virtme.stderr else setsid bash /run/tmp/.virtme-script < /dev/virtio-ports/virtme.stdin > /dev/virtio-ports/virtme.stdout 2> /dev/virtio-ports/virtme.stderr fi ret=$? log "script returned {$ret}" # Channel exit code to the host. if [ -e /dev/virtio-ports/virtme.ret ]; then echo ${ret} > /dev/virtio-ports/virtme.ret fi # Hmm. We should expose the return value somehow. sync poweroff -f exit 0 fi fi # Figure out what the main console is if [[ -n ${virtme_console} ]]; then consdev=${virtme_console} else consdev="$(grep ' ... (.C' /proc/consoles | cut -d' ' -f1)" fi if [[ -z $consdev ]]; then log "can't deduce console device" exec bash --login # At least try to be helpful fi if [[ -n ${virtme_user} ]]; then chown -- "${virtme_user}" /dev/"${consdev}" fi deallocvt if [[ $consdev == "tty0" ]]; then # Create some VTs openvt -c 2 -- /bin/bash openvt -c 3 -- /bin/bash openvt -c 4 -- /bin/bash consdev=tty1 # sigh fi if [[ ! -e "/dev/$consdev" ]]; then log "/dev/$consdev doesn't exist." exec bash --login fi # Redirect current stdout/stderr to consdev exec 1> "/dev/${consdev}" exec 2>&1 # Parameters that start with virtme_ shouldn't pollute the environment for p in "${!virtme_@}"; do export -n -- "${p?}" done # Welcome message echo " _ _ " echo " __ _(_)_ __| |_ _ __ ___ ___ _ __ __ _ " echo " \ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |" echo " \ V /| | | | |_| | | | | | __/_____| | | | (_| |" echo " \_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |" echo " |___/ " echo " kernel version: $(uname -mr)" echo " (CTRL+d to exit)" echo "" # Set up a basic environment (unless virtme-ng is running as root on the host) if [[ -z ${virtme_root_user} ]]; then install -d -m 0755 /run/tmp/roothome export HOME=/run/tmp/roothome mount --bind /run/tmp/roothome /root else export HOME=/root fi # $XDG_RUNTIME_DIR defines the base directory relative to which user-specific # non-essential runtime files and other file objects (such as sockets, named # pipes, ...) should be stored. if [[ -n ${virtme_user} ]]; then XDG_RUNTIME_DIR=/run/user/"$(id -u -- "${virtme_user}")" else XDG_RUNTIME_DIR=/run/user/"$(id -u)" fi export XDG_RUNTIME_DIR mkdir -p "$XDG_RUNTIME_DIR" if [[ -n ${virtme_user} ]]; then chown -- "${virtme_user}" "$XDG_RUNTIME_DIR" fi # Bring up a functioning shell on the console. This is a bit magical: # We have no controlling terminal because we're attached to a fake # console device (probably something like /dev/console), which can't # be a controlling terminal. We are also not a member of a session. # Init apparently can't setsid (whether that's a limitation of the # setsid binary or the system call, I don't know). if [[ -n ${virtme_stty_con} ]]; then # Program the console sensibly # shellcheck disable=SC2086 # The parameter is a white space separated array stty ${virtme_stty_con} < "/dev/$consdev" fi if [[ -n ${virtme_graphics} ]]; then # Check if we need to enable the sound system. if grep -q -E '(^| )virtme.sound($| )' /proc/cmdline; then pre_exec_cmd="$(dirname -- "$0")/virtme-sound-script" else pre_exec_cmd="" fi # Clean up any previous X11 state. rm -f /tmp/.X11*/* /tmp/.X11-lock # Create a .xinitrc to start the requested graphical application. xinit_rc=/run/tmp/.xinitrc echo -e "${pre_exec_cmd}\nexec /run/tmp/.virtme-script" > ${xinit_rc} chmod +x /run/tmp/.virtme-script if [[ -n ${virtme_user} ]]; then chown -- "${virtme_user}" ${xinit_rc} # Try to fix permissions on the virtual consoles, we are starting X # directly here so we may need extra permissions on the tty devices. chown -- "${virtme_user}" /dev/char/* setsid bash -c "su - ${virtme_user} -c 'xinit ${xinit_rc}'" 0<> "/dev/$consdev" 1>&0 2>&0 else setsid bash -c "xinit ${xinit_rc}" 0<> "/dev/$consdev" 1>&0 2>&0 fi # Drop to console if the graphical app failed. fi if [[ -n ${virtme_user} ]]; then setsid bash -c "su - ${virtme_user}" 0<> "/dev/$consdev" 1>&0 2>&0 else setsid bash 0<> "/dev/$consdev" 1>&0 2>&0 fi # Exit when the main shell session terminates sync poweroff -f exit 0 virtme-ng-1.35/virtme/guest/virtme-snapd-script000077500000000000000000000003531500572323100216470ustar00rootroot00000000000000#!/bin/bash # # Initialize a snap cgroup to emulate a systemd environment, tricking snapd # into recognizing our system as a valid one. mkdir /sys/fs/cgroup/snap.virtme.service echo 1 > /sys/fs/cgroup/snap.virtme.service/cgroup.procs virtme-ng-1.35/virtme/guest/virtme-sound-script000077500000000000000000000015041500572323100216710ustar00rootroot00000000000000#!/bin/bash if [ -n "$(command -v pipewire)" ]; then # Start audio system services. pipewire & wireplumber & pipewire-pulse & # Wait for pulseaudio backend to be up and running. for _ in $(seq 1 5); do pactl info && break sleep 1 done # Configure pulseaudio backend. pactl load-module module-combine-sink sink_name=combine pactl load-module module-null-sink sink_name=Virtme pactl load-module module-loopback sink=Virtme pactl load-module module-loopback sink=Virtme elif [ -n "$(command -v pulseaudio)" ]; then echo "WARNING: pulseaudio subsystem not supported yet" exit 1 elif [ -n "$(command -v jackd)" ]; then echo "WARNING: jack subsystem not supported yet" exit 1 else echo "WARNING: could not find a compatible sound subsystem" exit 1 fi virtme-ng-1.35/virtme/guest/virtme-sshd-script000077500000000000000000000054541500572323100215120ustar00rootroot00000000000000#!/bin/bash # # Initialize ssh server for remote connections (option `--server ssh`) if [ -z "${virtme_ssh_user}" ]; then echo "ssh: virtme_ssh_user is not defined" >&2 exit 1 fi if [ -z "${virtme_ssh_channel}" ]; then echo "ssh: virtme_ssh_channel is not defined" >&2 exit 1 fi rm -f /var/run/nologin # use an ssh location OUTSIDE of the users home directory to enable sharing # of host keys and authorized keys without dubious permission errors SSH_DIR=/run/sshd # use an ssh location INSIDE of the users home directory to communicate pre- # created hostkeys and authorized keys SSH_HOME="$(getent passwd "${virtme_ssh_user}" | cut -d: -f6)" if [ ! -e "${SSH_HOME}" ]; then # Setup an arbitrary ssh location, just to be able to start sshd. SSH_HOME="${SSH_DIR}" fi SSH_CACHE="${SSH_HOME}"/.cache/virtme-ng/.ssh # Generate ssh host keys (if they don't exist already). mkdir -p "${SSH_CACHE}"/etc/ssh ssh-keygen -A -f "${SSH_CACHE}" # copy hostkeys to server location mkdir -p "${SSH_DIR}"/etc/ssh cp "${SSH_CACHE}"/etc/ssh/* "${SSH_DIR}"/etc/ssh # Generate authorized_keys in the virtme-ng cache directory and add all # current user's public keys. SSH_AUTH_KEYS="${SSH_DIR}"/etc/ssh/authorized_keys cat "${SSH_HOME}"/.ssh/id_*.pub >> "${SSH_AUTH_KEYS}" 2> /dev/null # fixup permissions chown -R root:root "${SSH_DIR}"/etc/ssh chmod 600 "${SSH_DIR}"/etc/ssh/ssh_host_*_key chmod 644 "${SSH_DIR}"/etc/ssh/ssh_host_*_key.pub "${SSH_AUTH_KEYS}" # Determine sftp server (to support scp) sftp_server="" if [ -e /usr/lib/ssh/sftp-server ]; then sftp_server="Subsystem sftp /usr/lib/ssh/sftp-server" elif [ -e /usr/lib/openssh/sftp-server ]; then sftp_server="Subsystem sftp /usr/lib/openssh/sftp-server" fi # Generate a minimal sshd config. SSH_CONFIG="${SSH_DIR}"/etc/ssh/sshd_config cat << EOF > "${SSH_CONFIG}" # This file is automatically generated by virtme-ng. Port 22 PermitRootLogin yes AuthorizedKeysFile ${SSH_AUTH_KEYS} PubkeyAuthentication yes UsePAM yes PrintMotd no ${sftp_server} EOF # Start sshd. ARGS=(-f "${SSH_CONFIG}") for key in "${SSH_DIR}"/etc/ssh/ssh_host_*_key; do ARGS+=(-h "${key}") done if [[ ${virtme_ssh_channel} == "vsock" ]]; then # Make sure vsock (module) is loaded and active, otherwise the '/dev/vsock' device # might not be available. if ! modprobe vsock &> /dev/null; then echo "ssh: vsock module could not be loaded. SSHD cannot be started. Use '--ssh-tcp' instead to fallback to SSH over TCP." >&2 exit 1 fi # 4294967295 == U32_MAX == -1 declare -r VMADDR_CID_ANY=4294967295 # TODO Use something like syslog or journal for the logging setsid --fork -- systemd-socket-activate --accept --listen="vsock:${VMADDR_CID_ANY}:22" --inetd -- /usr/sbin/sshd -i "${ARGS[@]}" &> /dev/null < /dev/null else /usr/sbin/sshd "${ARGS[@]}" fi virtme-ng-1.35/virtme/guest/virtme-udhcpc-script000077500000000000000000000030731500572323100220120ustar00rootroot00000000000000#!/bin/bash # virtme-udhcpc-script: A trivial udhcpc script # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 # Make ShellCheck and co. happy. See https://udhcp.busybox.net/README.udhcpc declare interface ip mask router dns if [[ $1 == "deconfig" ]]; then ip link set dev "$interface" up ip addr flush dev "$interface" elif [[ $1 == "bound" ]]; then ip addr add "$ip/$mask" dev "$interface" if [[ -n $router ]]; then ip route add default via "$router" dev "$interface" fi if [[ -n $dns ]]; then # A lot of systems will have /etc/resolv.conf symlinked to # /run/NetworkManager/something_or_other. Debian symlinks to /run/resolvconf. # Create both directories. install -d /run/NetworkManager install -d /run/resolvconf real_resolv_conf=/etc/resolv.conf if [[ -L $real_resolv_conf ]]; then real_resolv_conf="/$(readlink /etc/resolv.conf)" if [[ ! -e $real_resolv_conf ]]; then mkdir -p "$(dirname "$real_resolv_conf")" touch "$real_resolv_conf" fi fi if [[ -f $real_resolv_conf ]]; then tmpfile="$(mktemp --tmpdir=/tmp)" chmod 644 "$tmpfile" mount --bind "$tmpfile" "$real_resolv_conf" fi echo -e "# Generated by virtme-udhcpc-script\n\nnameserver $dns" \ > /etc/resolv.conf fi fi virtme-ng-1.35/virtme/mkinitramfs.py000066400000000000000000000116471500572323100175650ustar00rootroot00000000000000# -*- mode: python -*- # virtme-mkinitramfs: Generate an initramfs image for virtme # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import io import os import shlex import tempfile from typing import Dict, List, Optional from . import cpiowriter, util def make_base_layout(cw): for d in ( b"lib", b"bin", b"var", b"etc", b"newroot", b"dev", b"proc", b"tmproot", b"run_virtme", b"run_virtme/data", b"run_virtme/guesttools", ): cw.mkdir(d, 0o755) cw.symlink(b"bin", b"sbin") cw.symlink(b"lib", b"lib64") def make_dev_nodes(cw): cw.mkchardev(b"dev/null", (1, 3), mode=0o666) cw.mkchardev(b"dev/kmsg", (1, 11), mode=0o666) cw.mkchardev(b"dev/console", (5, 1), mode=0o660) def install_busybox(cw, config): with open(config.busybox, "rb") as busybox: cw.write_file(name=b"bin/busybox", body=busybox, mode=0o755) for tool in ( "sh", "mount", "umount", "switch_root", "sleep", "mkdir", "mknod", "insmod", "cp", "cat", ): cw.symlink(b"busybox", (f"bin/{tool}").encode("ascii")) cw.mkdir(b"bin/real_progs", mode=0o755) _LOGFUNC = """log() { if [[ -e /dev/kmsg ]]; then echo "<6>virtme initramfs: $*" >/dev/kmsg else echo "virtme initramfs: $*" fi } """ def install_modprobe(cw): cw.write_file( name=b"bin/modprobe", body=b"\n".join( [ b"#!/bin/sh", _LOGFUNC.encode("utf-8"), b'log "initramfs does not have module $3"', b"exit 1", ] ), mode=0o755, ) def install_modules(cw, modfiles): cw.mkdir(b"modules", 0o755) paths = [] with tempfile.TemporaryDirectory() as tmpdirname: for mod in modfiles: if mod.endswith(".zst"): mod_file = os.path.basename(mod) uncompressed_mod = tmpdirname + "/" + os.path.splitext(mod_file)[0] os.system(f"zstd -d < {mod} > {uncompressed_mod}") mod = uncompressed_mod with open(mod, "rb") as f: modpath = "modules/" + os.path.basename(mod) paths.append(modpath) cw.write_file(name=modpath.encode("ascii"), body=f, mode=0o644) script = _LOGFUNC + "\n".join( f"log 'loading {os.path.basename(p)}...'; insmod {shlex.quote(p)}" for p in paths ) cw.write_file(name=b"modules/load_all.sh", body=script.encode("ascii"), mode=0o644) _INIT = r"""#!/bin/sh {logfunc} source /modules/load_all.sh log 'mounting hostfs...' if ! /bin/mount -n -t virtiofs -o {access} ROOTFS /newroot/ 2>/dev/null; then if ! /bin/mount -n -t 9p -o {access},version=9p2000.L,trans=virtio,access=any /dev/root /newroot/; then echo "Failed to mount real root. We are stuck." sleep 5 exit 1 fi fi # Can we actually use /newroot/ as root? if ! mount -t proc -o nosuid,noexec,nodev proc /newroot/proc 2>/dev/null; then # QEMU 1.5 and below have a bug in virtfs that prevents mounting # anything on top of a virtfs mount. log "your host's virtfs is broken -- using a fallback tmpfs" need_fallback_tmpfs=1 else umount /newroot/proc # Don't leave garbage behind fi # Find init mount -t proc none /proc for arg in `cat /proc/cmdline`; do if [[ "${{arg%%=*}}" = "init" ]]; then init="${{arg#init=}}" break fi done umount /proc if [[ -z "$init" ]]; then log 'no init= option' exit 1 fi log 'done; switching to real root' exec /bin/switch_root /newroot "$init" "$@" """ def generate_init(config) -> bytes: out = io.StringIO() out.write(_INIT.format(logfunc=_LOGFUNC, access=config.access)) return out.getvalue().encode("utf-8") class Config: __slots__ = ["modfiles", "virtme_data", "virtme_init_path", "busybox", "access"] def __init__(self): self.modfiles: List[str] = [] self.virtme_data: Dict[bytes, bytes] = {} self.virtme_init_path: Optional[str] = None self.busybox: Optional[str] = None self.access = "ro" def mkinitramfs(out, config) -> None: cw = cpiowriter.CpioWriter(out) make_base_layout(cw) make_dev_nodes(cw) install_busybox(cw, config) install_modprobe(cw) if config.modfiles is not None: install_modules(cw, config.modfiles) for name, contents in config.virtme_data.items(): cw.write_file(b"run_virtme/data/" + name, body=contents, mode=0o755) cw.write_file(b"init", body=generate_init(config), mode=0o755) cw.write_trailer() def find_busybox(root, is_native) -> Optional[str]: return util.find_binary( ["busybox-static", "busybox.static", "busybox"], root=root, use_path=is_native ) virtme-ng-1.35/virtme/modfinder.py000066400000000000000000000041671500572323100172070ustar00rootroot00000000000000# -*- mode: python -*- # modfinder: A simple tool to resolve required modules # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 """ This is a poor man's module resolver and loader. It does not support any sort of hotplug. Instead it generates a topological order and loads everything. The idea is to require very few modules. """ import itertools import platform import re import subprocess from typing import List from . import util _INSMOD_RE = re.compile("insmod (.*[^ ]) *$") def resolve_dep(modalias, root=None, kver=None, moddir=None): # /usr/sbin might not be in the path, and modprobe is usually in /usr/sbin modprobe = util.find_binary_or_raise(["modprobe"]) args = [modprobe, "--show-depends"] args += ["-C", "/var/empty"] if root is not None: args += ["-d", root] if kver is not None and kver != platform.release(): # If booting the loaded kernel, skip -S. This helps certain # buggy modprobe versions that don't support -S. args += ["-S", kver] if moddir is not None: args += ["--moddir", moddir] args += ["--", modalias] deps = [] try: with open("/dev/null", "r+b") as devnull: script = subprocess.check_output(args, stderr=devnull.fileno()).decode( "utf-8", errors="replace" ) for line in script.split("\n"): m = _INSMOD_RE.match(line) if m: deps.append(m.group(1)) except subprocess.CalledProcessError: pass # This is most likely because the module is built in. return deps def merge_mods(lists) -> List[str]: found: set = set() mods = [] for mod in itertools.chain(*lists): if mod not in found: found.add(mod) mods.append(mod) return mods def find_modules_from_install(aliases, root=None, kver=None, moddir=None): return merge_mods( resolve_dep(a, root=root, kver=kver, moddir=moddir) for a in aliases ) virtme-ng-1.35/virtme/qemu_helpers.py000066400000000000000000000035431500572323100177260ustar00rootroot00000000000000# -*- mode: python -*- # qemu_helpers: Helpers to find QEMU and handle its quirks # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import os import platform import re import shutil import subprocess from typing import Optional class Qemu: qemubin: str version: Optional[str] def __init__(self, qemubin, arch) -> None: self.arch = arch self.has_multidevs = None self.cannot_overmount_virtfs = None if not qemubin: qemubin = shutil.which(f"qemu-system-{arch}") if qemubin is None and arch == platform.machine(): qemubin = shutil.which("qemu-kvm") if qemubin is None: raise ValueError(f"cannot find qemu for {arch}") else: if not os.path.isfile(qemubin): raise ValueError(f'specified qemu binary "{qemubin}" does not exist') if not os.access(qemubin, os.X_OK): raise ValueError(f'specified qemu binary "{qemubin}" is not executable') self.qemubin = qemubin self.version = None def probe(self) -> None: if self.version is None: self.version = subprocess.check_output([self.qemubin, "--version"]).decode( "utf-8" ) self.cannot_overmount_virtfs = ( re.search(r"version 1\.[012345]", self.version) is not None ) # QEMU 4.2+ supports -fsdev multidevs=remap self.has_multidevs = ( re.search(r"version (?:1\.|2\.|3\.|4\.[01][^\d])", self.version) is None ) def quote_optarg(self, a: str) -> str: """Quote an argument to an option.""" return a.replace(",", ",,") virtme-ng-1.35/virtme/resources.py000066400000000000000000000025341500572323100172460ustar00rootroot00000000000000# -*- mode: python -*- # resources.py: Find virtme's resources # Copyright © 2014-2019 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 """Helpers to find virtme's guest tools and host scripts.""" import shutil import subprocess from importlib import resources as importlib_resources import virtme def find_guest_tools(): """Return the path of the guest tools installed with the running virtme.""" if importlib_resources.files(virtme).joinpath("guest").is_dir(): return str(importlib_resources.files(virtme) / "guest") # No luck. This is somewhat surprising. return None def find_script(name) -> str: # If we're running out of a source checkout, we can find scripts in the # 'bin' directory. fn = importlib_resources.files(virtme) / "../bin" / name if fn.is_file(): return str(fn) # Otherwise assume we're actually installed and in PATH. guess = shutil.which(name) if guess is not None: return guess # No luck. This is somewhat surprising. raise FileNotFoundError(f"could not find script {name}") def run_script(name, **kwargs) -> None: fn = find_script(name) subprocess.check_call(executable=fn, args=[fn], **kwargs) virtme-ng-1.35/virtme/util.py000066400000000000000000000032511500572323100162060ustar00rootroot00000000000000# -*- mode: python -*- # util.py: Misc helpers # Copyright © 2014-2019 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 import getpass import itertools import os import shutil from typing import Optional, Sequence class SilentError(Exception): pass def get_username(): """Reliably get current username.""" try: username = getpass.getuser() except OSError: # If getpass.getuser() fails, try alternative methods username = os.getenv("USER") or os.getenv("LOGNAME") return username def check_kernel_repo(): if not os.path.isfile("scripts/kconfig/merge_config.sh") and not os.path.isfile( "source/scripts/kconfig/merge_config.sh" ): return False return True def find_binary( names: Sequence[str], root: str = "/", use_path: bool = True ) -> Optional[str]: dirs = [ os.path.join(*i) for i in itertools.product(["usr/local", "usr", ""], ["bin", "sbin"]) ] for n in names: if use_path: # Search PATH first path = shutil.which(n) if path is not None: return path for d in dirs: path = os.path.join(root, d, n) if os.path.isfile(path): return path # We give up. return None def find_binary_or_raise( names: Sequence[str], root: str = "/", use_path: bool = True ) -> str: ret = find_binary(names, root=root, use_path=use_path) if ret is None: raise RuntimeError(f"Could not find {names!r}") return ret virtme-ng-1.35/virtme/virtmods.py000066400000000000000000000021651500572323100171030ustar00rootroot00000000000000# -*- mode: python -*- # virtmods: Default module configuration # Copyright © 2014 Andy Lutomirski # Licensed under the GPLv2, which is available in the virtme distribution # as a file called LICENSE with SHA-256 hash: # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 MODALIASES = [ # These are most likely portable across all architectures. "fs-9p", "fs-virtiofs", "virtio:d00000009v00001AF4", # 9pnet_virtio "virtio:d00000003v00001AF4", # virtio_console # These are required by the microvm architecture. "virtio_pci", # virtio-pci "virtio_mmio", # virtio-mmio # For virtio_pci architectures (which are, hopefully, all that we care # about), there's really only one required driver, virtio_pci. # For completeness, here are both of the instances we care about # for basic functionality. "pci:v00001AF4d00001009sv00001AF4sd00000009bc00sc02i00", # 9pnet "pci:v00001AF4d00001003sv00001AF4sd00000003bc07sc80i00", # virtconsole # Basic system functionality "unix", # UNIX sockets, needed by udev # Basic emulated hardware "i8042", "atkbd", ] virtme-ng-1.35/virtme_ng/000077500000000000000000000000001500572323100153425ustar00rootroot00000000000000virtme-ng-1.35/virtme_ng/__init__.py000066400000000000000000000000001500572323100174410ustar00rootroot00000000000000virtme-ng-1.35/virtme_ng/mainline.py000066400000000000000000000057261500572323100175220ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: mainline kernel downloader.""" import os import re import subprocess import sys from glob import glob from shutil import which import requests from virtme_ng.utils import CACHE_DIR, spinner_decorator BASE_URL = "https://kernel.ubuntu.com/mainline" HTTP_CHUNK = 4096 HTTP_TIMEOUT = 30 class KernelDownloader: def __init__(self, version, arch="amd64", verbose=False): # Fetch and extract precompiled mainline kernel self.kernel_dir = f"{CACHE_DIR}/{version}/{arch}" self.version = version self.arch = arch self.verbose = verbose self.target = f"{self.kernel_dir}/boot/vmlinuz*generic" if not glob(self.target): self._fetch_kernel() def _download_file(self, url, destination): response = requests.get(url, stream=True, timeout=HTTP_TIMEOUT) if response.status_code == 200: os.makedirs(self.kernel_dir, exist_ok=True) with open(destination, "wb") as file: for chunk in response.iter_content(chunk_size=HTTP_CHUNK): file.write(chunk) else: raise FileNotFoundError( f"failed to download {url}, error: {response.status_code}" ) @spinner_decorator(message="📥 downloading kernel") def _fetch_kernel(self): if not which("dpkg"): raise FileNotFoundError( "dpkg is not available, unable to uncompress kernel deb" ) url = BASE_URL + "/" + self.version + "/" + self.arch response = requests.get(url, timeout=HTTP_TIMEOUT) if response.status_code != 200: url = BASE_URL + "/" + self.version response = requests.get(url, timeout=HTTP_TIMEOUT) if self.verbose: sys.stderr.write( f"use {self.version}/{self.arch} pre-compiled kernel from {url}\n" ) if response.status_code == 200: href_pattern = re.compile(r'href=["\']([^\s"\']+.deb)["\']') matches = href_pattern.findall(response.text) for match in matches: # Skip headers packages if "headers" in match: continue # Skip packages for different architectures if f"{self.arch}.deb" not in match: continue # Skip if package is already downloaded deb_file = f"{self.kernel_dir}/{match}" if os.path.exists(deb_file): continue self._download_file(url + "/" + match, deb_file) subprocess.check_call(["dpkg", "-x", deb_file, self.kernel_dir]) if not glob(f"{self.kernel_dir}/*.deb"): raise FileNotFoundError(f"could not find kernel packages at {url}") else: raise FileNotFoundError( f"failed to retrieve content, error: {response.status_code}" ) virtme-ng-1.35/virtme_ng/run.py000066400000000000000000001436661500572323100165400ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: main command-line frontend.""" import argparse import json import os import platform import re import shlex import shutil import signal import socket import sys import tempfile from pathlib import Path from select import select from subprocess import ( DEVNULL, PIPE, CalledProcessError, Popen, check_call, check_output, ) import argcomplete from virtme.util import SilentError, get_username from virtme_ng.mainline import KernelDownloader from virtme_ng.utils import CONF_FILE, spinner_decorator from virtme_ng.version import VERSION def check_call_cmd(command, quiet=False, dry_run=False): if dry_run: print(shlex.join(command)) return with Popen( command, stdout=PIPE, stderr=PIPE, stdin=DEVNULL, ) as process: process.stdout.flush() process.stderr.flush() stdout_fd = process.stdout.fileno() stderr_fd = process.stderr.fileno() stdout_open = True stderr_open = True # Use select to poll for new data in the file descriptors while stdout_open or stderr_open: ready_to_read, _, _ = select([stdout_fd, stderr_fd], [], [], 1) for fd in ready_to_read: if fd == stdout_fd: line = process.stdout.readline().decode() if line: if not quiet: sys.stdout.write(line) sys.stdout.flush() else: stdout_open = False elif fd == stderr_fd: line = process.stderr.readline().decode() if line: sys.stderr.write(line) sys.stderr.flush() else: stderr_open = False # Wait for the process to complete and get the return code return_code = process.wait() # Trigger a CalledProcessError exception if command failed if return_code: raise CalledProcessError(return_code, command) def make_parser(): """Main virtme-ng command line parser.""" parser = argparse.ArgumentParser( prog="vng", formatter_class=argparse.RawTextHelpFormatter, description="Build and run kernels inside a virtualized snapshot of your live system", epilog="""\ virtme-ng is a tool that allows to easily and quickly recompile and test a Linux kernel, starting from the source code. It allows to re‐ compile the kernel in a few minutes (rather than hours), then the kernel is automatically started in a virtualized environment that is an exact copy-on-write copy of your live system, which means that any changes made to the virtualized environment do not affect the host system. In order to do this, a minimal config is produced (with the bare minimum support to test the kernel inside qemu), then the selected kernel is automatically built and started inside qemu, using the filesystem of the host as a copy-on-write snapshot. This means that you can safely destroy the entire filesystem, crash the kernel, etc. without affecting the host. NOTE: kernels produced with virtme-ng are lacking lots of features, in order to reduce the build time to the minimum and still provide you a usable kernel capable of running your tests and experiments. virtme-ng is based on virtme, written by Andy Lutomirski . """, ) parser.add_argument( "--version", "-V", action="version", version=f"virtme-ng {VERSION}" ) g_action = parser.add_argument_group(title="Action").add_mutually_exclusive_group() g_action.add_argument( "--run", "-r", action="store", nargs="?", const=platform.release(), default=None, help="Run a specified kernel; " "--run can accept one of the following arguments: 1) nothing (in this " "case it'll try to boot the same kernel running on the host), 2) a kernel " "binary (like ./arch/x86/boot/bzImage), 3) a directory (where it'll try " "to find a valid kernel binary file), 4) an upstream version, for " "example `vng --run v6.6.17` (in this case vng will download a " "precompiled upstream kernel from the Ubuntu mainline repository)", ) g_action.add_argument( "--build", "-b", action="store_true", help="Build the kernel in the current directory " "(or remotely if used with --build-host)", ) g_action.add_argument( "--clean", "-x", action="store_true", help="Clean the kernel repository (local or remote if used with --build-host)", ) g_action.add_argument( "--dump", "-d", action="store", help="Generate a memory dump of the running kernel " "(instance needs to be started with --debug)", ) parser.add_argument( "--dry-run", action="store_true", help="Only show the commands without actually running them.", ) parser.add_argument( "--skip-config", "-s", action="store_true", help="[deprecated] Do not re-generate kernel .config", ) parser.add_argument( "--no-virtme-ng-init", action="store_true", help="Fallback to the bash virtme-init (useful for debugging/development)", ) parser.add_argument( "--gdb", action="store_true", help="Attach a debugging session to a running instance started with --debug", ) parser.add_argument( "--snaps", action="store_true", help="Allow to execute snaps inside virtme-ng" ) parser.add_argument( "--debug", action="store_true", help="Start the instance with debugging enabled (allow to generate crash dumps)", ) parser.add_argument( "--kconfig", "-k", action="store_true", help="Only override the kernel .config without building/running anything", ) parser.add_argument( "--skip-modules", "-S", action="store_true", help="Run a really fast build by skipping external modules " "(no external modules support)", ) parser.add_argument( "--commit", "-c", action="store", help="Use a kernel identified by a specific commit id, tag or branch", ) parser.add_argument( "--config", "--custom", "-f", action="append", help="Use one (or more) specific kernel .config snippet " "to override default config settings", ) parser.add_argument( "--configitem", action="append", help="add a CONFIG_ITEM=val, after --config , " "these override previous config settings", ) parser.add_argument( "--compiler", action="store", help="[deprecated] Compiler to be used as CC when building the kernel. " "Please set CC= and HOSTCC= variables in the virtme-ng command line instead.", ) parser.add_argument( "--busybox", metavar="PATH_TO_BUSYBOX", action="store", help="Use the specified busybox binary", ) parser.add_argument("--qemu", action="store", help="Use the specified QEMU binary") parser.add_argument( "--name", action="store", default="virtme-ng", help="Set guest hostname and qemu -name flag", ) parser.add_argument( "--user", action="store", help="Change user inside the guest (default is same user as the host)", ) parser.add_argument( "--root", action="store", help="Pass a specific chroot to use inside the virtualized kernel " + "(useful with --arch)", ) parser.add_argument( "--root-release", action="store", help="Use a target Ubuntu release to create a new chroot (used with --root)", ) parser.add_argument( "--rw", action="store_true", help="Give the guest read-write access to its root filesystem. " "WARNING: this can be dangerous for the host filesystem!", ) parser.add_argument( "--force-9p", action="store_true", help="Use legacy 9p filesystem as rootfs" ) parser.add_argument( "--disable-microvm", action="store_true", help='Avoid using the "microvm" QEMU architecture (only on x86_64)', ) parser.add_argument( "--disable-kvm", action="store_true", help="Avoid using hardware virtualization / KVM", ) parser.add_argument( "--disable-monitor", action="store_true", help="Disable QEMU STDIO monitor" ) parser.add_argument( "--cwd", action="store", help="Change guest working directory " + "(default is current working directory when possible)", ) parser.add_argument( "--pwd", action="store_true", help="[deprecated] --pwd is set implicitly by default", ) parser.add_argument( "--rodir", action="append", default=[], help="Supply a read-only directory to the guest. " + "Use --rodir=path or --rodir=guestpath=hostpath", ) parser.add_argument( "--rwdir", action="append", default=[], help="Supply a read/write directory to the guest. " + "Use --rwdir=path or --rwdir=guestpath=hostpath", ) parser.add_argument( "--overlay-rwdir", action="append", default=[], help="Supply a directory that is r/w to the guest but read-only in the host. " + "Use --overlay-rwdir=path.", ) parser.add_argument( "--cpus", "-p", action="store", help="Set guest CPU count (qemu -smp flag)" ) parser.add_argument( "--memory", "-m", action="store", help="Set guest memory size (qemu -m flag)" ) parser.add_argument( "--numa", metavar="MEM[,cpus=FIRST_CPU1[-LAST_CPU1]][,cpus=FIRST_CPU2[-LAST_CPU2]]...", action="append", help="Create a NUMA node in the guest. " + "Use this option multiple times to create more NUMA nodes. " + "The total memory size assigned to NUMA nodes must match the guest memory size (specified with --memory/-m). " + "This option implicitly disables the microvm architecture.", ) parser.add_argument( "--numa-distance", metavar="SRC,DST=VAL", action="append", help="Set a distance of VAL between NUMA node SRC_NODE and DST_NODE. " + "Use this option multiple times to define multiple distances between NUMA nodes. " + "This option is used only together with --numa.", ) parser.add_argument( "--balloon", action="store_true", help="Allow the host to ask the guest to release memory", ) parser.add_argument( "--network", "-n", action="append", help="Enable network access: user, bridge(=
), loop", ) parser.add_argument( "--net-mac-address", action="store", help="The MAC address to assign to the NIC interface, e.g. 52:54:00:12:34:56. " + "The last octet will be incremented for the next network devices.", ) parser.add_argument( "--disk", "-D", action="append", metavar="PATH", help="Add a file as virtio-scsi disk (can be used multiple times)", ) parser.add_argument( "--exec", "-e", action="store", help="Execute a command inside the kernel and exit", ) parser.add_argument( "--append", "-a", action="append", help="Additional kernel boot options (can be used multiple times)", ) parser.add_argument( "--force-initramfs", action="store_true", help="Use an initramfs even if unnecessary", ) parser.add_argument( "--sound", action="store_true", help="Enable audio device (if the architecture supports it)", ) parser.add_argument( "--graphics", "-g", action="store_true", help="Show graphical output instead of using a console.", ) parser.add_argument( "--verbose", "-v", action="store_true", help="Increase console output verbosity.", ) parser.add_argument( "--quiet", "-q", action="store_true", help="Override verbose mode (disable --verbose).", ) parser.add_argument( "--qemu-opts", "-o", action="append", help="Additional arguments for QEMU (can be used multiple times)" " or bundled together: --qemu-opts='...'", ) parser.add_argument( "--build-host", action="store", help="Perform kernel build on a remote server (ssh access required)", ) parser.add_argument( "--build-host-exec-prefix", action="store", help="Prepend a command (e.g., chroot) " "to the make command executed on the remote build host", ) parser.add_argument( "--build-host-vmlinux", action="store_true", help="Copy vmlinux back from the build host", ) parser.add_argument( "--arch", action="store", help="Generate and test a kernel for a specific architecture " "(default is host architecture ; if set, to be used with --root)", ) parser.add_argument( "--cross-compile", action="store", help="Set cross-compile prefix" ) parser.add_argument( "--force", action="store_true", help="Force reset git repository to target branch or commit " "(warning: this may drop uncommitted changes), " "and force kernel config override", ) parser.add_argument( "envs", metavar="envs", type=str, nargs="*", help="Additional Makefile variables", ) parser.add_argument( "--nvgpu", action="store", metavar="[GPU PCI Address]", help="Add a passthrough NVIDIA GPU", ) g_remote = parser.add_argument_group(title="Remote Console") g_remote.add_argument( "--console", action="store", nargs="?", type=int, const=2222, metavar="PORT", help="Enable a server to communicate later from the host using '--console-client'. " + "By default, a simple console will be offered using a VSOCK connection, and 'socat' for the proxy.", ) g_remote.add_argument( "--console-client", action="store", nargs="?", type=int, const=2222, metavar="PORT", help="Connect to a VM launched with the '--console' option for a remote control.", ) g_remote.add_argument( "--ssh", action="store", nargs="?", type=int, const=2222, metavar="PORT", help="Enable SSH server to communicate later from the host to using '--ssh-client'.", ) g_remote.add_argument( "--ssh-client", action="store", nargs="?", type=int, const=2222, metavar="PORT", help="Connect to a VM launched with the '--ssh' option for a remote control.", ) g_remote.add_argument( "--ssh-tcp", action="store_true", help="Use TCP for the SSH connection to the guest", ) g_remote.add_argument( "--remote-cmd", action="store", metavar="COMMAND", help="To start in the VM a different command than the default one (--server), " + "or to launch this command instead of a prompt (--client).", ) return parser _ARGPARSER = make_parser() def arg_fail(message, show_usage=True): """Print an error message and exit, optionally showing usage help.""" sys.stderr.write(message + "\n") if show_usage: _ARGPARSER.print_usage() sys.exit(1) ARCH_MAPPING = { "amd64": { "qemu_name": "x86_64", "linux_name": "x86_64", "cross_compile": "x86_64-linux-gnu-", "kernel_target": "bzImage", "kernel_image": "bzImage", }, "arm64": { "qemu_name": "aarch64", "linux_name": "arm64", "cross_compile": "aarch64-linux-gnu-", "kernel_target": "Image", "kernel_image": "Image", }, "armhf": { "qemu_name": "arm", "linux_name": "arm", "cross_compile": "arm-linux-gnueabihf-", "kernel_target": "", "kernel_image": "zImage", "max-cpus": 4, }, "ppc64el": { "qemu_name": "ppc64", "linux_name": "powerpc", "cross_compile": "powerpc64le-linux-gnu-", "kernel_target": "vmlinux", "kernel_image": "vmlinux", }, "s390x": { "qemu_name": "s390x", "linux_name": "s390", "cross_compile": "s390x-linux-gnu-", "kernel_target": "bzImage", "kernel_image": "bzImage", }, "riscv64": { "qemu_name": "riscv64", "linux_name": "riscv", "cross_compile": "riscv64-linux-gnu-", "kernel_target": "Image", "kernel_image": "Image", }, # adding a new arch? Please also update get_host_arch(). } REMOTE_BUILD_SCRIPT = """#!/bin/bash cd ~/.virtme git reset --hard __virtme__ [ -f debian/rules ] && fakeroot debian/rules clean {} {} """ def create_root(destdir, arch, release): """Initialize a rootfs directory, populating files/directory if it doesn't exist.""" if os.path.exists(destdir): return # Use Ubuntu's cloud images to create a rootfs, these images are fairly # small and they provide a nice environment to test kernels. if release is None: try: release = ( check_output("lsb_release -s -c", shell=True) .decode(sys.stdout.encoding) .rstrip() ) if release == "n/a": raise ValueError("unknown release") except (CalledProcessError, ValueError): print( "Unknown release, try specifying an Ubuntu release with --root-release" ) sys.exit(1) url = ( "https://cloud-images.ubuntu.com/" + f"{release}/current/{release}-server-cloudimg-{arch}-root.tar.xz" ) prevdir = os.getcwd() os.system(f"sudo mkdir -p {destdir}") os.chdir(destdir) os.system(f"curl -s {url} | sudo tar xvJ") os.chdir(prevdir) def get_host_arch(): """Translate host architecture to the corresponding virtme-ng arch name.""" arch = platform.machine() arch_map = { "x86_64": "amd64", "aarch64": "arm64", "armv7l": "armhf", "ppc64le": "ppc64el", "riscv64": "riscv64", "s390x": "s390x", } return arch_map.get(arch, None) class KernelSource: """Main class that implement actions to perform on a kernel source directory.""" def __init__(self): self.virtme_param = {} conf_path = self.get_conf_file_path() self.default_opts = [] if conf_path is not None: with open(conf_path, encoding="utf-8") as conf_fd: conf_data = json.loads(conf_fd.read()) if "default_opts" in conf_data: self.default_opts = conf_data["default_opts"] self.cpus = str(os.cpu_count()) def get_conf_file_path(self): """Return virtme-ng main configuration file path.""" # First check if there is a config file in the user's home config # directory, then check for a single config file in ~/.virtme-ng.conf and # finally check for /etc/virtme-ng.conf. If none of them exist, report an # error and exit. configs = ( CONF_FILE, Path(Path.home(), ".virtme-ng.conf"), Path("/etc", "virtme-ng.conf"), ) for conf in configs: if conf.exists(): return conf return None def _format_cmd(self, cmd): return shlex.split(cmd) def _is_dirty_repo(self): cmd = "git --no-optional-locks status -uno --porcelain" if check_output(self._format_cmd(cmd), stderr=DEVNULL, stdin=DEVNULL): return True return False def checkout(self, args): """Perform a git checkout operation on a local kernel git repository.""" if not os.path.exists(".git"): arg_fail("error: must run from a kernel git repository", show_usage=False) target = args.commit or "HEAD" if args.build_host is not None or target != "HEAD": if not args.force and self._is_dirty_repo(): arg_fail( "error: you have uncommitted changes in your git repository, " + "use --force to drop them", show_usage=False, ) check_call_cmd( ["git", "reset", "--hard", target], quiet=not args.verbose, dry_run=args.dry_run, ) def config(self, args): """Perform a make config operation on a kernel source directory.""" arch = args.arch cmd = ["virtme-configkernel", "--defconfig"] if args.verbose: cmd.append("--verbose") if not args.force and not args.kconfig: cmd.append("--no-update") if arch is not None: if arch not in ARCH_MAPPING: arg_fail(f"unsupported architecture: {arch}") arch = ARCH_MAPPING[arch]["qemu_name"] cmd += ["--arch", arch] user_config = str(Path.home()) + "/.config/virtme-ng/kernel.config" if os.path.exists(user_config): cmd += ["--custom", user_config] if args.config: for conf in args.config: cmd += ["--custom", conf] if args.configitem: for citem in args.configitem: cmd += ["--configitem", citem] # Propagate additional Makefile variables cmd += args.envs if args.verbose: print(f"cmd: {shlex.join(cmd)}") check_call_cmd(cmd, quiet=not args.verbose, dry_run=args.dry_run) def _make_remote(self, args, make_command): check_call_cmd( ["ssh", args.build_host, "mkdir -p ~/.virtme"], quiet=not args.verbose, dry_run=args.dry_run, ) check_call_cmd( ["ssh", args.build_host, "git init ~/.virtme"], quiet=not args.verbose, dry_run=args.dry_run, ) check_call_cmd( [ "git", "push", "--force", "--porcelain", f"{args.build_host}:~/.virtme", "HEAD:refs/heads/__virtme__", ], quiet=not args.verbose, dry_run=args.dry_run, ) cmd = f"rsync .config {args.build_host}:.virtme/.config" check_call_cmd( self._format_cmd(cmd), quiet=not args.verbose, dry_run=args.dry_run ) # Create remote build script with tempfile.NamedTemporaryFile(mode="w+t") as tmp: tmp.write( REMOTE_BUILD_SCRIPT.format( args.build_host_exec_prefix or "", make_command + " -j$(nproc --all)", ) ) tmp.flush() cmd = f"rsync {tmp.name} {args.build_host}:.virtme/.kc-build" check_call_cmd( self._format_cmd(cmd), quiet=not args.verbose, dry_run=args.dry_run ) # Execute remote build script check_call_cmd( ["ssh", args.build_host, "bash", ".virtme/.kc-build"], quiet=not args.verbose, dry_run=args.dry_run, ) # Copy artifacts back to the running host with tempfile.NamedTemporaryFile(mode="w+t") as tmp: if args.build_host_vmlinux or args.arch == "ppc64el": vmlinux = "--include=vmlinux" else: vmlinux = "" if args.skip_modules: cmd = ( "rsync -azS --progress --exclude=.config --exclude=.git/ " + "--include=*/ --include=bzImage --include=zImage --include=Image " + f'{vmlinux} --include=*.dtb --exclude="*" {args.build_host}:.virtme/ ./' ) else: cmd = ( "rsync -azS --progress --exclude=.config --exclude=.git/ " + '--include=*/ --include="*.ko" --include=".dwo" ' + f"--include=bzImage --include=zImage --include=Image {vmlinux} " + "--include=.config --include=modules.* " + "--include=System.map --include=Module.symvers --include=module.lds " + '--include=*.dtb --include="**/generated/**" --exclude="*" ' + f"{args.build_host}:.virtme/ ./" ) tmp.write(cmd) tmp.flush() check_call_cmd( ["bash", tmp.name], quiet=not args.verbose, dry_run=args.dry_run ) if not args.skip_modules: if os.path.exists("./debian/rules"): check_call_cmd( ["fakeroot", "debian/rules", "clean"], quiet=not args.verbose ) check_call_cmd( self._format_cmd( make_command + f" -j {self.cpus}" + " modules_prepare" ), quiet=not args.verbose, dry_run=args.dry_run, ) def make(self, args): """Perform a make operation on a kernel source directory.""" if not os.path.exists(".git") and args.build_host is not None: arg_fail( "error: --build-host can be used only on a kernel git repository", show_usage=False, ) if args.build_host is not None and self._is_dirty_repo(): arg_fail( "error: you have uncommitted changes in your git repository, " + "commit or drop them before building on a remote host", show_usage=False, ) arch = args.arch if arch is not None: if arch not in ARCH_MAPPING: arg_fail(f"unsupported architecture: {arch}") target = ARCH_MAPPING[arch]["kernel_target"] cross_compile = ARCH_MAPPING[arch]["cross_compile"] if args.cross_compile: cross_compile = args.cross_compile cross_arch = ARCH_MAPPING[arch]["linux_name"] else: target = "bzImage" cross_compile = None cross_arch = None make_command = ["make"] if args.skip_modules: make_command.append(target) make_command.append("LOCALVERSION=-virtme") if args.compiler: make_command += [f"HOSTCC={args.compiler}", f"CC={args.compiler}"] if cross_compile and cross_arch: make_command += [f"CROSS_COMPILE={cross_compile}", f"ARCH={cross_arch}"] # Propagate additional Makefile variables make_command += args.envs make_command += ["-j", self.cpus] if args.verbose: print(f"cmd: {shlex.join(make_command)}") if args.build_host is None: # Build the kernel locally check_call_cmd(make_command, quiet=not args.verbose, dry_run=args.dry_run) else: # Build the kernel on a remote build host self._make_remote(args, make_command) def _get_virtme_name(self, args): if args.name is not None: self.virtme_param["name"] = "--name " + args.name else: self.virtme_param["name"] = "--name " + socket.gethostname() def _get_virtme_exec(self, args): envs = [] for var in args.envs: if var.startswith("O="): self.virtme_param["kdir"] = "--kdir " + var[2:] else: envs.append(var) if envs: args.exec = " ".join(envs) if args.exec is not None: self.virtme_param["exec"] = f"--script-sh {shlex.quote(args.exec)}" else: self.virtme_param["exec"] = "" def _get_virtme_user(self, args): # Default user for scripts is root, default user for interactive # sessions is current user. # # NOTE: graphic sessions are considered interactive. self.virtme_param["user"] = "" if args.exec and not args.graphics: self.virtme_param["user"] = "" else: self.virtme_param["user"] = "--user " + get_username() # Override default user, if specified by the --user argument. if args.user is not None: self.virtme_param["user"] = "--user " + args.user def _get_virtme_arch(self, args): if args.arch is not None: if args.arch not in ARCH_MAPPING: arg_fail( f"unsupported architecture ({args.arch}), " f"available: {' '.join(ARCH_MAPPING)}", show_usage=False, ) if args.root is None and get_host_arch() != args.arch: arg_fail("--arch used without --root") if "max-cpus" in ARCH_MAPPING[args.arch]: self.cpus = ARCH_MAPPING[args.arch]["max-cpus"] self.virtme_param["arch"] = "--arch " + ARCH_MAPPING[args.arch]["qemu_name"] else: self.virtme_param["arch"] = "" def _get_virtme_root(self, args): if args.root is not None: create_root(args.root, args.arch or get_host_arch(), args.root_release) self.virtme_param["root"] = f"--root {args.root}" else: self.virtme_param["root"] = "" def _get_virtme_rw(self, args): if args.rw: self.virtme_param["rw"] = "--rw" else: self.virtme_param["rw"] = "" def _get_virtme_cwd(self, args): if args.cwd is not None: if args.pwd: arg_fail("--pwd and --cwd are mutually exclusive") self.virtme_param["cwd"] = "--cwd " + args.cwd elif args.root is None: self.virtme_param["cwd"] = "--pwd" else: self.virtme_param["cwd"] = "" def _get_virtme_rodir(self, args): self.virtme_param["rodir"] = "" for item in args.rodir: self.virtme_param["rodir"] += f"--rodir {item} " def _get_virtme_rwdir(self, args): self.virtme_param["rwdir"] = "" for item in args.rwdir: self.virtme_param["rwdir"] += f"--rwdir {item} " def _get_virtme_overlay_rwdir(self, args): # Set default overlays if rootfs is mounted in read-only mode. if args.rw: self.virtme_param["overlay_rwdir"] = "" else: self.virtme_param["overlay_rwdir"] = " ".join( f"--overlay-rwdir {d}" for d in ( "/etc", "/lib", "/home", "/opt", "/srv", "/usr", "/var", "/tmp", ) ) # Add user-specified overlays. for item in args.overlay_rwdir: self.virtme_param["overlay_rwdir"] += " --overlay-rwdir " + item def _get_virtme_run(self, args): if args.run is not None: # If an upstream version is specified (using an upstream tag) fetch # and run the corresponding kernel from the Ubuntu mainline # repository. if re.match(r"^v\d+(\.\d+)*(-rc\d+)?$", args.run): if args.arch is None: arch = get_host_arch() else: arch = args.arch try: mainline = KernelDownloader( args.run, arch=arch, verbose=args.verbose ) self.virtme_param["kdir"] = "--kimg " + mainline.target except FileNotFoundError as exc: sys.stderr.write(str(exc) + "\n") sys.exit(1) else: self.virtme_param["kdir"] = "--kimg " + args.run elif self.virtme_param.get("kdir") is None: kbuild_dir = os.environ.get("KBUILD_OUTPUT") if kbuild_dir is None or not os.path.isdir(kbuild_dir): kbuild_dir = "./" self.virtme_param["kdir"] = "--kdir " + kbuild_dir def _get_virtme_mods(self, args): if args.skip_modules or platform.system() != "Linux": self.virtme_param["mods"] = "--mods none" else: self.virtme_param["mods"] = "--mods auto" def _get_virtme_dry_run(self, args): if args.dry_run: self.virtme_param["dry_run"] = "--show-command --dry-run" else: self.virtme_param["dry_run"] = "" def _get_virtme_no_virtme_ng_init(self, args): if args.no_virtme_ng_init: self.virtme_param["no_virtme_ng_init"] = "--no-virtme-ng-init" else: self.virtme_param["no_virtme_ng_init"] = "" def _get_virtme_network(self, args): if args.network is not None: network_str = " ".join([f"--net {network}" for network in args.network]) self.virtme_param["network"] = network_str else: self.virtme_param["network"] = "" def _get_virtme_net_mac_address(self, args): if args.net_mac_address is not None: self.virtme_param["net_mac_address"] = ( "--net-mac-address " + args.net_mac_address ) else: self.virtme_param["net_mac_address"] = "" def _get_virtme_console(self, args): if args.console is not None: self.virtme_param["console"] = f"--server console --port {args.console}" else: self.virtme_param["console"] = "" def _get_virtme_console_client(self, args): if args.console is not None and args.console_client is not None: arg_fail("--console cannot be used with --console-client", show_usage=False) if args.console_client is not None: self.virtme_param["console_client"] = ( f"--client console --port {args.console_client}" ) else: self.virtme_param["console_client"] = "" def _get_virtme_ssh(self, args): if args.console is not None and args.ssh is not None: arg_fail("--console cannot be used with --ssh", show_usage=False) if args.ssh is not None: self.virtme_param["ssh"] = f"--server ssh --port {args.ssh}" else: self.virtme_param["ssh"] = "" def _get_virtme_disable_monitor(self, args): if args.disable_monitor: self.virtme_param["disable_monitor"] = "--disable-monitor" else: self.virtme_param["disable_monitor"] = "" def _get_virtme_ssh_client(self, args): if args.console_client is not None and args.ssh_client is not None: arg_fail( "--console-client cannot be used with --ssh-client", show_usage=False ) if args.ssh is not None and args.ssh_client is not None: arg_fail("--ssh cannot be used with --ssh-client", show_usage=False) if args.console is not None and args.ssh_client is not None: arg_fail("--console cannot be used with --ssh-client", show_usage=False) if args.ssh_client is not None: self.virtme_param["ssh_client"] = f"--client ssh --port {args.ssh_client}" else: self.virtme_param["ssh_client"] = "" def _get_virtme_remote_cmd(self, args): if args.remote_cmd is not None: self.virtme_param["remote_cmd"] = ( f"--remote-cmd {shlex.quote(args.remote_cmd)}" ) elif args.envs and ( args.console_client is not None or args.ssh_client is not None or args.console is not None ): self.virtme_param["remote_cmd"] = ( f"--remote-cmd {shlex.quote(shlex.join(args.envs))}" ) else: self.virtme_param["remote_cmd"] = "" def _get_virtme_disk(self, args): if args.disk is not None: disk_str = "" for dsk in args.disk: disk_str += f"--blk-disk {dsk}={dsk} " self.virtme_param["disk"] = disk_str else: self.virtme_param["disk"] = "" def _get_virtme_sound(self, args): if args.sound: self.virtme_param["sound"] = "--sound" else: self.virtme_param["sound"] = "" def _get_virtme_vmcoreinfo(self, args): if args.debug: # Enable vmcoreinfo (required by drgn memory dumps) self.virtme_param["vmcoreinfo"] = "--vmcoreinfo" else: self.virtme_param["vmcoreinfo"] = "" def _get_virtme_disable_microvm(self, args): # Automatically disable microvm in debug mode, since it seems to # produce incomplete memory dumps. if args.disable_microvm or args.debug: self.virtme_param["disable_microvm"] = "--disable-microvm" else: self.virtme_param["disable_microvm"] = "" def _get_virtme_disable_kvm(self, args): if args.disable_kvm: self.virtme_param["disable_kvm"] = "--disable-kvm" else: self.virtme_param["disable_kvm"] = "" def _get_virtme_ssh_tcp(self, args): if args.ssh_tcp: self.virtme_param["ssh_tcp"] = "--ssh-tcp" else: self.virtme_param["ssh_tcp"] = "" def _get_virtme_9p(self, args): if args.force_9p: self.virtme_param["force_9p"] = "--force-9p" else: self.virtme_param["force_9p"] = "" def _get_virtme_initramfs(self, args): if args.force_initramfs: self.virtme_param["force_initramfs"] = "--force-initramfs" else: self.virtme_param["force_initramfs"] = "" def _get_virtme_graphics(self, args): if args.graphics: self.virtme_param["graphics"] = "--graphics" else: self.virtme_param["graphics"] = "" def _get_virtme_verbose(self, args): if args.verbose: self.virtme_param["verbose"] = "--verbose --show-boot-console" else: self.virtme_param["verbose"] = "" def _get_virtme_append(self, args): append = [] if args.append is not None: for item in args.append: split_items = shlex.split(item) for split_item in split_items: append += ["-a", split_item] if args.debug: append += ["-a", "nokaslr"] self.virtme_param["append"] = shlex.join(append) def _get_virtme_memory(self, args): if args.memory is None: self.virtme_param["memory"] = "--memory 1G" else: self.virtme_param["memory"] = "--memory " + args.memory def _get_virtme_numa(self, args): if args.numa is not None: numa_str = " ".join([f"--numa {numa}" for numa in args.numa]) self.virtme_param["numa"] = numa_str else: self.virtme_param["numa"] = "" def _get_virtme_numa_distance(self, args): if args.numa_distance is not None: if not args.numa: arg_fail( "error: --numa-distance can be used only with --numa", show_usage=False, ) numa_dist_str = "" for arg in args.numa_distance: try: nodes = arg.split("=") src, dst = nodes[0].split(",") val = nodes[1] numa_dist_str += f" --numa-distance src={src},dst={dst},val={val}" except ValueError: err_msg = ( f"error: invalid distance '{arg}', " + "NUMA distance string must be in the format SRC,DST=VAL" ) arg_fail(err_msg, show_usage=False) self.virtme_param["numa_distance"] = numa_dist_str else: self.virtme_param["numa_distance"] = "" def _get_virtme_balloon(self, args): if args.balloon: self.virtme_param["balloon"] = "--balloon" else: self.virtme_param["balloon"] = "" def _get_virtme_gdb(self, args): if args.gdb: def signal_handler(_signum, _frame): pass # No action needed for SIGINT in child (gdb will handle) signal.signal(signal.SIGINT, signal_handler) self.virtme_param["gdb"] = "--gdb" else: self.virtme_param["gdb"] = "" def _get_virtme_snaps(self, args): if args.snaps: self.virtme_param["snaps"] = "--snaps" else: self.virtme_param["snaps"] = "" def _get_virtme_busybox(self, args): if args.busybox is not None: self.virtme_param["busybox"] = "--busybox " + args.busybox else: self.virtme_param["busybox"] = "" def _get_virtme_qemu(self, args): if args.qemu is not None: self.virtme_param["qemu"] = "--qemu-bin " + args.qemu else: self.virtme_param["qemu"] = "" def _get_virtme_cpus(self, args): if args.cpus is None: cpus = self.cpus else: cpus = args.cpus self.virtme_param["cpus"] = f"--cpus {cpus}" def _get_virtme_nvgpu(self, args): if args.nvgpu is not None: self.virtme_param["nvgpu"] = f"--nvgpu 'vfio-pci,host={args.nvgpu}'" else: self.virtme_param["nvgpu"] = "" def _get_virtme_qemu_opts(self, args): qemu_args = "" if args.debug: # Enable debug mode and QMP (to trigger memory dump via `vng --dump`) qemu_args += "-s -qmp tcp:localhost:3636,server,nowait " if args.qemu_opts is not None: qemu_args += " ".join(args.qemu_opts) if qemu_args != "": self.virtme_param["qemu_opts"] = "--qemu-opts " + qemu_args else: self.virtme_param["qemu_opts"] = "" def run(self, args): """Execute a kernel inside virtme-ng.""" self._get_virtme_name(args) self._get_virtme_exec(args) self._get_virtme_user(args) self._get_virtme_arch(args) self._get_virtme_root(args) self._get_virtme_rw(args) self._get_virtme_rodir(args) self._get_virtme_rwdir(args) self._get_virtme_overlay_rwdir(args) self._get_virtme_cwd(args) self._get_virtme_run(args) self._get_virtme_dry_run(args) self._get_virtme_no_virtme_ng_init(args) self._get_virtme_mods(args) self._get_virtme_network(args) self._get_virtme_net_mac_address(args) self._get_virtme_console(args) self._get_virtme_console_client(args) self._get_virtme_ssh(args) self._get_virtme_ssh_client(args) self._get_virtme_remote_cmd(args) self._get_virtme_disk(args) self._get_virtme_sound(args) self._get_virtme_vmcoreinfo(args) self._get_virtme_disable_microvm(args) self._get_virtme_disable_monitor(args) self._get_virtme_disable_kvm(args) self._get_virtme_ssh_tcp(args) self._get_virtme_9p(args) self._get_virtme_initramfs(args) self._get_virtme_graphics(args) self._get_virtme_verbose(args) self._get_virtme_append(args) self._get_virtme_cpus(args) self._get_virtme_memory(args) self._get_virtme_numa(args) self._get_virtme_numa_distance(args) self._get_virtme_balloon(args) self._get_virtme_gdb(args) self._get_virtme_snaps(args) self._get_virtme_busybox(args) self._get_virtme_nvgpu(args) self._get_virtme_qemu(args) self._get_virtme_qemu_opts(args) # Start VM using virtme-run cmd = ( "virtme-run " + f"{self.virtme_param['name']} " + f"{self.virtme_param['exec']} " + f"{self.virtme_param['user']} " + f"{self.virtme_param['arch']} " + f"{self.virtme_param['root']} " + f"{self.virtme_param['rw']} " + f"{self.virtme_param['rodir']} " + f"{self.virtme_param['rwdir']} " + f"{self.virtme_param['overlay_rwdir']} " + f"{self.virtme_param['cwd']} " + f"{self.virtme_param['kdir']} " + f"{self.virtme_param['dry_run']} " + f"{self.virtme_param['no_virtme_ng_init']} " + f"{self.virtme_param['mods']} " + f"{self.virtme_param['network']} " + f"{self.virtme_param['net_mac_address']} " + f"{self.virtme_param['console']} " + f"{self.virtme_param['console_client']} " + f"{self.virtme_param['ssh']} " + f"{self.virtme_param['ssh_client']} " + f"{self.virtme_param['remote_cmd']} " + f"{self.virtme_param['disk']} " + f"{self.virtme_param['sound']} " + f"{self.virtme_param['vmcoreinfo']} " + f"{self.virtme_param['disable_microvm']} " + f"{self.virtme_param['disable_monitor']} " + f"{self.virtme_param['disable_kvm']} " + f"{self.virtme_param['ssh_tcp']} " + f"{self.virtme_param['force_9p']} " + f"{self.virtme_param['force_initramfs']} " + f"{self.virtme_param['graphics']} " + f"{self.virtme_param['verbose']} " + f"{self.virtme_param['append']} " + f"{self.virtme_param['cpus']} " + f"{self.virtme_param['memory']} " + f"{self.virtme_param['numa']} " + f"{self.virtme_param['numa_distance']} " + f"{self.virtme_param['balloon']} " + f"{self.virtme_param['gdb']} " + f"{self.virtme_param['snaps']} " + f"{self.virtme_param['busybox']} " + f"{self.virtme_param['nvgpu']} " + f"{self.virtme_param['qemu']} " + f"{self.virtme_param['qemu_opts']} " # Important: qemu_opts has to be the last one ) check_call(cmd, shell=True) def dump(self, args): """Generate or analyze a crash memory dump.""" # Use QMP to generate a memory dump sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("localhost", 3636)) data = sock.recv(1024) if not data: sys.exit(1) if args.verbose: sys.stdout.write(data.decode("utf-8")) sock.send(b'{ "execute": "qmp_capabilities" }\r') data = sock.recv(1024) if not data: sys.exit(1) if args.verbose: sys.stdout.write(data.decode("utf-8")) dump_file = args.dump with tempfile.NamedTemporaryFile(delete=dump_file is None) as tmp: msg = ( '{"execute":"dump-guest-memory",' '"arguments":{"paging":true,' '"protocol":"file:' + tmp.name + '"}}' "\r" ) if args.verbose: sys.stdout.write(msg + "\n") sock.send(msg.encode("utf-8")) data = sock.recv(1024) if not data: sys.exit(1) if args.verbose: sys.stdout.write(data.decode("utf-8")) data = sock.recv(1024) if args.verbose: sys.stdout.write(data.decode("utf-8")) # Save memory dump to target file shutil.move(tmp.name, dump_file) def clean(self, args): """Clean a local or remote git repository.""" if not os.path.exists(".git"): arg_fail("error: must run from a kernel git repository", show_usage=False) if args.build_host is None: cmd = self._format_cmd("git clean -xdf") else: cmd = f"ssh {args.build_host} --" cmd = self._format_cmd(cmd) cmd.append("cd ~/.virtme && git clean -xdf") check_call_cmd(cmd, quiet=not args.verbose, dry_run=args.dry_run) @spinner_decorator(message="📦 checking out kernel") def checkout(kern_source, args): """Checkout kernel.""" kern_source.checkout(args) return True @spinner_decorator(message="🔧 configuring kernel") def config(kern_source, args): """Configure the kernel.""" kern_source.config(args) return True @spinner_decorator(message="⚙️c building kernel") def make(kern_source, args): """Build the kernel.""" kern_source.make(args) return True @spinner_decorator(message="🧹 cleaning kernel") def clean(kern_source, args): """Clean the kernel repo.""" kern_source.clean(args) return True def run(kern_source, args): """Run the kernel.""" return kern_source.run(args) @spinner_decorator(message="🐞 generating memory dump") def dump(kern_source, args): """Dump the kernel (if the kernel was running with --debug).""" kern_source.dump(args) return True def do_it() -> int: """Main body.""" argcomplete.autocomplete(_ARGPARSER) args = _ARGPARSER.parse_args() kern_source = KernelSource() if kern_source.default_opts: for opt in kern_source.default_opts: val = kern_source.default_opts[opt] setattr(args, opt, val) if args.verbose and args.quiet: args.verbose = False try: if args.clean: clean(kern_source, args) elif args.dump is not None: dump(kern_source, args) elif args.build or args.kconfig: if args.commit: checkout(kern_source, args) config(kern_source, args) if args.kconfig: return 0 make(kern_source, args) else: try: run(kern_source, args) return 0 except CalledProcessError as exc: return exc.returncode except CalledProcessError as exc: raise SilentError() from exc return 0 def main() -> int: """Main.""" try: return do_it() except (KeyboardInterrupt, SilentError): return 1 if __name__ == "__main__": main() virtme-ng-1.35/virtme_ng/spinner.py000066400000000000000000000070701500572323100173760ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: UI spinner class.""" import sys import threading import time from queue import Queue class InterceptedStream: """Fake stream class used to intercept original sys.stdout and sys.stderr.""" def __init__(self, queue): self.queue = queue def write(self, text): """Intercept original stream write() and push output to a thread queue.""" self.queue.put(text) def flush(self): """Intercept original stream flush() that becomes a no-op.""" class Spinner: """A live spinner to keep track of lengthy operations. The code block inside the context of a Spinner will have the stdout and stderr intercepted, so that the output is properly synchronized with the spinner text (no interleaving text). If the code is executed in a headless environment, e.g., without a valid tty, all features are disabled. Example usage: >>> from virtme_ng.spinner import Spinner ... with Spinner(message='Processing') as spin: ... for i in range(10): ... sys.stderr.write('hello\n') ... time.sleep(1) Args: message (Optional[str]): an optional, always visible message """ def __init__(self, message=""): self.message = message self.spinner_str = "▁▂▃▄▅▆▇██▇▆▅▄▃▂▁" self.pos = 0 self.stop_event = threading.Event() self.spinner_thread = threading.Thread(target=self._spin) self.spinner_thread.daemon = True self.original_streams = {} self.intercepted_streams = {} self.start_time = int(time.time()) self.is_tty = sys.stdout.isatty() def __enter__(self): if self.is_tty: self.original_streams = { "stdout": sys.stdout, "stderr": sys.stderr, } self.intercepted_streams = { "stdout": Queue(), "stderr": Queue(), } sys.stdout = InterceptedStream(self.intercepted_streams["stdout"]) sys.stderr = InterceptedStream(self.intercepted_streams["stderr"]) self.spinner_thread.start() return self def __exit__(self, exc_type, exc_val, exc_tb): if self.is_tty: self.stop_event.set() self.spinner_thread.join() self._flush_streams() sys.stdout = self.original_streams["stdout"] sys.stderr = self.original_streams["stderr"] def _flush_streams(self): stdout = self.intercepted_streams["stdout"] stderr = self.intercepted_streams["stderr"] orig_stdout = self.original_streams["stdout"] orig_stderr = self.original_streams["stderr"] for stream, orig_stream in [(stdout, orig_stdout), (stderr, orig_stderr)]: while not stream.empty(): orig_stream.write(stream.get()) orig_stream.flush() def _spinner_line(self): self.pos = (self.pos + 1) % len(self.spinner_str) spinner = self.spinner_str[self.pos :] + self.spinner_str[: self.pos] delta_t = int(time.time()) - self.start_time header = f"{spinner[:3]} {self.message} ({delta_t} sec)\033[?25l" spacer = f"\r{' ' * len(header)}\r" stdout = self.original_streams["stdout"] stdout.write(header) stdout.flush() time.sleep(0.1) stdout.write(spacer + "\033[?25h") def _spin(self): while not self.stop_event.is_set(): self._flush_streams() self._spinner_line() virtme-ng-1.35/virtme_ng/utils.py000066400000000000000000000015021500572323100170520ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: configuration path.""" from pathlib import Path from virtme_ng.spinner import Spinner CACHE_DIR = Path(Path.home(), ".cache", "virtme-ng") SSH_DIR = Path(CACHE_DIR, ".ssh") SSH_CONF_FILE = SSH_DIR.joinpath("virtme-ng-ssh.conf") VIRTME_SSH_DESTINATION_NAME = "virtme-ng" VIRTME_SSH_HOSTNAME_CID_SEPARATORS = ("%", "/") DEFAULT_VIRTME_SSH_HOSTNAME_CID_SEPARATOR = VIRTME_SSH_HOSTNAME_CID_SEPARATORS[0] CONF_PATH = Path(Path.home(), ".config", "virtme-ng") CONF_FILE = Path(CONF_PATH, "virtme-ng.conf") def spinner_decorator(message): def decorator(func): def wrapper(*args, **kwargs): with Spinner(message=message): result = func(*args, **kwargs) return result return wrapper return decorator virtme-ng-1.35/virtme_ng/version.py000066400000000000000000000035061500572323100174050ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng version""" import os from importlib.metadata import PackageNotFoundError, version from subprocess import DEVNULL, CalledProcessError, check_output PKG_VERSION = "1.35" def get_package_version(): try: return version("virtme-ng") except PackageNotFoundError: return PKG_VERSION def get_version_string(): if os.environ.get("VNG_PACKAGE"): return PKG_VERSION if not os.environ.get("__VNG_LOCAL"): return get_package_version() try: # Get the version from `git describe`. # # Make sure to get the proper git repository by using the directory # that contains this file and also make sure that the parent is a # virtme-ng repository. # # Otherwise fallback to the static version defined in PKG_VERSION. version = ( check_output( f"cd {os.path.dirname(__file__)} && [ -e ../.git ] && git describe --long --dirty", shell=True, stderr=DEVNULL, ) .decode("utf-8") .strip() ) # Remove the 'v' prefix if present if version.startswith("v"): version = version[1:] # Replace hyphens with plus sign for build metadata version_pep440 = version.replace("-", "+", 1).replace("-", ".") return version_pep440 except CalledProcessError: # If git describe fails to determine a version (e.g. building from the # source using a tarball), the version from pip cannot be picked because # it might be different than the local one being used here. Fall back to # the hard-coded package version then. return PKG_VERSION VERSION = get_version_string() if __name__ == "__main__": print(VERSION) virtme-ng-1.35/virtme_ng_init/000077500000000000000000000000001500572323100163655ustar00rootroot00000000000000virtme-ng-1.35/virtme_ng_init/.gitignore000066400000000000000000000000101500572323100203440ustar00rootroot00000000000000/target virtme-ng-1.35/virtme_ng_init/Cargo.lock000066400000000000000000000034521500572323100202760ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "uzers" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4df81ff504e7d82ad53e95ed1ad5b72103c11253f39238bcc0235b90768a97dd" dependencies = [ "libc", "log", ] [[package]] name = "virtme-ng-init" version = "0.1.0" dependencies = [ "base64", "nix", "uzers", ] virtme-ng-1.35/virtme_ng_init/Cargo.toml000066400000000000000000000004661500572323100203230ustar00rootroot00000000000000[package] name = "virtme-ng-init" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] nix = { version = "0.29", features = ["feature", "fs", "hostname", "mount", "reboot", "user"] } uzers = "0.12" base64 = "0.22" virtme-ng-1.35/virtme_ng_init/LICENSE000066400000000000000000001045151500572323100174000ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . virtme-ng-1.35/virtme_ng_init/README.md000066400000000000000000000036331500572323100176510ustar00rootroot00000000000000# virtme-ng-init: fast init process for virtme-ng virtme-ng-init is an extremely lightweight init process for virtme-ng [1] implemented in Rust. Its primary goal is to speed up the boot time of virtme-ng instances. virtme-ng-init is able to perform any necessary initialization in the virtualized environment, such as mounting filesystems, starting essential services, and configuring the system before handing over control to the main user-space processes (typicall a shell session). [1] https://github.com/arighi/virtme-ng # Result - virtme-init (bash implementation): ``` $ time virtme-ng --exec 'uname -r' 6.4.0-rc3-virtme real 0m1.146s user 0m0.829s sys 0m1.048s $ time virtme-ng --net user --exec 'ip addr show dev eth0' 2: eth0: mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 scope global eth0 valid_lft forever preferred_lft forever real 0m1.282s user 0m0.930s sys 0m1.219s ``` - virtme-ng-init (Rust implementation): ``` $ time virtme-ng --exec 'uname -r' 6.4.0-rc3-virtme real 0m0.906s user 0m0.654s sys 0m0.684s $ time virtme-ng --net user --exec 'ip addr show dev eth0' 2: eth0: mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 scope global eth0 valid_lft forever preferred_lft forever real 0m0.972s user 0m0.736s sys 0m0.795s ``` # Building Static building is necessary as this binary is going to be executed before the file system is up and running. ``` RUSTFLAGS='-C target-feature=+crt-static' cargo build -r ``` # Local installation Put the binary into virtme/guest/bin/. e.g. when used as a submodule: ``` cp target/release/virtme-ng-init ../virtme/guest/bin ``` # Credits Author: Andrea Righi virtme-ng-1.35/virtme_ng_init/src/000077500000000000000000000000001500572323100171545ustar00rootroot00000000000000virtme-ng-1.35/virtme_ng_init/src/main.rs000066400000000000000000001053201500572323100204470ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0 //! virtme-ng-init //! //! This program serves as an extremely lightweight init process for `virtme-ng` in order to speed //! up boot time. //! //! Its primary purpose is to perform any necessary initialization in the virtualized environment, //! such as mounting filesystems, starting essential services, and configuring the system before //! handing over control to the main user-space processes (typicall a shell session). //! //! Author: Andrea Righi use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::Engine as _; use nix::fcntl::{open, OFlag}; use nix::libc; use nix::sys::reboot; use nix::sys::stat::Mode; use nix::sys::utsname::uname; use nix::unistd::sethostname; use std::env; use std::fs::{File, OpenOptions}; use std::io::{self, BufRead, BufReader, BufWriter, Write}; use std::os::fd::{AsRawFd, IntoRawFd}; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::{exit, id, Command, Stdio}; use std::thread; use std::time::Duration; #[macro_use] mod utils; #[cfg(test)] mod test; struct MountInfo { source: &'static str, target: &'static str, fs_type: &'static str, flags: usize, fsdata: &'static str, } const KERNEL_MOUNTS: &[MountInfo] = &[ MountInfo { source: "proc", target: "/proc", fs_type: "proc", flags: (libc::MS_NOSUID | libc::MS_NOEXEC | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "sys", target: "/sys", fs_type: "sysfs", flags: (libc::MS_NOSUID | libc::MS_NOEXEC | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "run", target: "/run", fs_type: "tmpfs", flags: 0, fsdata: "mode=0755", }, MountInfo { source: "devtmpfs", target: "/dev", fs_type: "devtmpfs", flags: (libc::MS_NOSUID | libc::MS_NOEXEC) as usize, fsdata: "", }, MountInfo { source: "configfs", target: "/sys/kernel/config", fs_type: "configfs", flags: 0, fsdata: "", }, MountInfo { source: "debugfs", target: "/sys/kernel/debug", fs_type: "debugfs", flags: 0, fsdata: "", }, MountInfo { source: "tracefs", target: "/sys/kernel/tracing", fs_type: "tracefs", flags: 0, fsdata: "", }, MountInfo { source: "securityfs", target: "/sys/kernel/security", fs_type: "securityfs", flags: 0, fsdata: "", }, ]; const SYSTEM_MOUNTS: &[MountInfo] = &[ MountInfo { source: "devpts", target: "/dev/pts", fs_type: "devpts", flags: (libc::MS_NOSUID | libc::MS_NOEXEC) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/dev/shm", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/log", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/tmp", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/spool/rsyslog", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/portables", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/machines", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/private", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/sudo", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/apt", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/cache", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, MountInfo { source: "tmpfs", target: "/var/lib/snapd/cookie", fs_type: "tmpfs", flags: (libc::MS_NOSUID | libc::MS_NODEV) as usize, fsdata: "", }, ]; const USER_SCRIPT: &str = "/run/tmp/.virtme-script"; fn check_init_pid() { if id() != 1 { log!("must be run as PID 1"); exit(1); } } fn poweroff() { unsafe { libc::sync(); } match reboot::reboot(reboot::RebootMode::RB_POWER_OFF) { Ok(_) => exit(0), Err(err) => { log!("error powering off: {}", err); exit(1); } } } fn configure_environment() { env::set_var("PATH", "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin"); } fn get_kernel_version(show_machine: bool) -> String { let utsname = match uname() { Ok(utsname) => utsname, Err(_) => return "None".to_string(), }; let release = utsname.release().to_string_lossy(); if show_machine { let machine = utsname.machine().to_string_lossy(); format!("{} {}", release, machine) } else { release.into_owned() } } fn get_legacy_active_console() -> Option { // See Documentation/filesystems/proc.rst for /proc/consoles documentation. match File::open("/proc/consoles") { Ok(file) => { let reader = BufReader::new(file); // .flatten() ignores lines with reading errors for line in reader.lines().flatten() { if line.chars().nth(27) == Some('C') { let console = line.split(' ').next()?; return Some(format!("/dev/{}", console)); } } None } Err(error) => { log!("failed to open /proc/consoles: {}", error); None } } } fn get_active_console() -> Option { if let Ok(console) = env::var("virtme_console") { Some(format!("/dev/{}", console)) } else { get_legacy_active_console() } } fn configure_limits() { if let Ok(nr_open) = env::var("nr_open") { if let Ok(mut file) = OpenOptions::new().write(true).open("/proc/sys/fs/nr_open") { file.write_all(nr_open.as_bytes()) .expect("Failed to write nr_open"); } } } fn configure_hostname() { if let Ok(hostname) = env::var("virtme_hostname") { if let Err(err) = sethostname(hostname) { log!("failed to change hostname: {}", err); } } else { log!("virtme_hostname is not defined"); } } fn run_systemd_tmpfiles() { if !Path::new("/etc/systemd").exists() { return; } let args: &[&str] = &[ "--create", "--boot", "--exclude-prefix=/dev", "--exclude-prefix=/root", ]; utils::run_cmd("systemd-tmpfiles", args); } fn generate_fstab() -> io::Result<()> { utils::create_file("/run/tmp/fstab", 0o0664, "").ok(); utils::do_mount( "/run/tmp/fstab", "/etc/fstab", "", libc::MS_BIND as usize, "", ); Ok(()) } fn generate_shadow() -> io::Result<()> { utils::create_file("/run/tmp/shadow", 0o0644, "").ok(); let input_file = File::open("/etc/passwd")?; let output_file = File::create("/run/tmp/shadow")?; let reader = BufReader::new(input_file); let mut writer = BufWriter::new(output_file); for line in reader.lines() { if let Some((username, _)) = line?.split_once(':') { writeln!(writer, "{}:!:::::::", username)?; } } utils::do_mount( "/run/tmp/shadow", "/etc/shadow", "", libc::MS_BIND as usize, "", ); Ok(()) } fn generate_sudoers() -> io::Result<()> { let fname = "/run/tmp/sudoers"; let mut content = "Defaults secure_path=\"/usr/sbin:/usr/bin:/sbin:/bin\"\n".to_string(); content += "root ALL = (ALL) NOPASSWD: ALL\n"; if let Ok(user) = env::var("virtme_user") { content += &format!("{} ALL = (ALL) NOPASSWD: ALL\n", user); } if !Path::new("/etc/sudoers").exists() { utils::create_file("/etc/sudoers", 0o0440, "").unwrap_or_else(|_| {}); } utils::create_file(fname, 0o0440, &content).ok(); utils::do_mount(fname, "/etc/sudoers", "", libc::MS_BIND as usize, ""); Ok(()) } // The /etc/lvm is usually only read/write by root. In order to allow commands like pvcreate to be // run on rootless users just create a dummy directory and bind mount it in the same place. fn generate_lvm() -> io::Result<()> { utils::do_mkdir("/run/tmp/lvm"); utils::do_mount("/run/tmp/lvm", "/etc/lvm/", "", libc::MS_BIND as usize, ""); Ok(()) } fn generate_hosts() -> io::Result<()> { if let Ok(hostname) = env::var("virtme_hostname") { std::fs::copy("/etc/hosts", "/run/tmp/hosts")?; let mut h = OpenOptions::new() .write(true) .append(true) .open("/run/tmp/hosts")?; writeln!(h, "\n127.0.0.1 {}\n::1 {}", hostname, hostname)?; utils::do_mount( "/run/tmp/hosts", "/etc/hosts", "", libc::MS_BIND as usize, "", ); } Ok(()) } fn override_system_files() { generate_fstab().ok(); generate_shadow().ok(); generate_sudoers().ok(); generate_hosts().ok(); generate_lvm().ok(); } fn set_cwd() { if let Ok(dir) = env::var("virtme_chdir") { if let Err(err) = env::set_current_dir(dir) { log!("error changing directory: {}", err); } } } fn symlink_fds() { let fd_links = [ ("/proc/self/fd", "/dev/fd"), ("/proc/self/fd/0", "/dev/stdin"), ("/proc/self/fd/1", "/dev/stdout"), ("/proc/self/fd/2", "/dev/stderr"), ]; // Install /proc/self/fd symlinks into /dev if not already present. for (src, dst) in fd_links.iter() { if !std::path::Path::new(dst).exists() { utils::do_symlink(src, dst); } } } fn mount_kernel_filesystems() { for mount_info in KERNEL_MOUNTS { // In the case where a rootfs is specified when launching virtme-ng, it // mounts /run and /run/virtme/guesttools prior to executing // virtme-ng-init. We do not want to re-mount /run, as we will lose // access to guesttools, which is required for network setup. // // Note, get_test_tools_dir() relies on /proc, so that must be mounted // prior to /run. if mount_info.target == "/run" { if let Some(guest_tools_dir) = get_guest_tools_dir() { if guest_tools_dir.starts_with("/run") { log!("/run previously mounted, skipping"); continue; } } } utils::do_mount( mount_info.source, mount_info.target, mount_info.fs_type, mount_info.flags, mount_info.fsdata, ) } } fn mount_cgroupfs() { // If SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 is passed we can mimic systemd's behavior and mount // the legacy cgroup v1 layout. let cmdline = std::fs::read_to_string("/proc/cmdline").unwrap(); if cmdline.contains("SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1") { utils::do_mount("cgroup", "/sys/fs/cgroup", "tmpfs", 0, ""); let subsystems = vec!["cpu", "cpuacct", "blkio", "memory", "devices", "pids"]; for subsys in &subsystems { let target = format!("/sys/fs/cgroup/{}", subsys); utils::do_mkdir(&target); // Don't treat failure as critical here, since the kernel may not // support all the legacy cgroups. utils::do_mount(subsys, &target, "cgroup", 0, subsys); } } else { utils::do_mount("cgroup2", "/sys/fs/cgroup", "cgroup2", 0, ""); } } fn mount_virtme_overlays() { utils::do_mkdir("/run/tmp/"); for (key, path) in env::vars() { if key.starts_with("virtme_rw_overlay") { let dir = &format!("/run/tmp/{}", key); let upperdir = &format!("{}/upper", dir); let workdir = &format!("{}/work", dir); let mnt_opts = &format!( "xino=off,lowerdir={},upperdir={},workdir={}", path, upperdir, workdir ); utils::do_mkdir(dir); utils::do_mkdir(upperdir); utils::do_mkdir(workdir); let result = utils::do_mount_check(&key, &path, "overlay", 0, mnt_opts); if result.is_err() { // Old kernels don't support xino=on|off, re-try without this option. let mnt_opts = &format!( "lowerdir={},upperdir={},workdir={}", path, upperdir, workdir ); utils::do_mount(&key, &path, "overlay", 0, mnt_opts); } } } } fn mount_virtme_initmounts() { for (key, path) in env::vars() { if key.starts_with("virtme_initmount") { utils::do_mkdir(&path); utils::do_mount( &key.replace('_', "."), &path, "9p", 0, "version=9p2000.L,trans=virtio,access=any", ); } } } fn mount_kernel_modules() { let kver = get_kernel_version(false); let mod_dir = format!("/lib/modules/{}", kver); // Make sure to always have /lib/modules, otherwise we won't be able to configure kmod support // properly (this can happen in some container environments, such as docker). if !Path::new(&mod_dir).exists() { utils::do_mkdir("/lib/modules"); } if env::var("virtme_root_mods").is_ok() { // /lib/modules is already set up. } else if let Ok(dir) = env::var("virtme_link_mods") { utils::do_mount("none", "/lib/modules/", "tmpfs", 0, ""); utils::do_symlink(&dir, &mod_dir); } else if Path::new(&mod_dir).exists() { // We have mismatched modules. Mask them off. utils::do_mount("disallow_kmod", &mod_dir, "tmpfs", 0, "ro,mode=0000"); } } fn mount_sys_filesystems() { utils::do_mkdir("/dev/pts"); utils::do_mkdir("/dev/shm"); utils::do_mkdir("/run/dbus"); for mount_info in SYSTEM_MOUNTS { utils::do_mount( mount_info.source, mount_info.target, mount_info.fs_type, mount_info.flags, mount_info.fsdata, ) } } fn fix_dpkg_locks() { if !Path::new("/var/lib/dpkg").exists() { return; } let lock_files = [ "/var/lib/dpkg/lock", "/var/lib/dpkg/lock-frontend", "/var/lib/dpkg/triggers/Lock", ]; for path in lock_files { let fname = Path::new(path) .file_name() .and_then(|name| name.to_str()) .unwrap_or(""); if fname.is_empty() { continue; } let src_file = format!("/run/tmp/{}", fname); utils::create_file(&src_file, 0o0640, "").ok(); utils::do_mount(&src_file, path, "", libc::MS_BIND as usize, ""); } } fn fix_packaging_files() { fix_dpkg_locks(); } fn disable_uevent_helper() { let uevent_helper_path = "/sys/kernel/uevent_helper"; if Path::new(uevent_helper_path).exists() { // This kills boot performance. log!("you have CONFIG_UEVENT_HELPER on, turn it off"); let mut file = OpenOptions::new().write(true).open(uevent_helper_path).ok(); match &mut file { Some(file) => { write!(file, "").ok(); } None => { log!("error opening {}", uevent_helper_path); } } } } fn find_udevd() -> Option { let static_candidates = [ PathBuf::from("/usr/lib/systemd/systemd-udevd"), PathBuf::from("/lib/systemd/systemd-udevd"), ]; let path = env::var("PATH").unwrap_or_else(|_| String::new()); let path_candidates = path.split(':').map(|dir| Path::new(dir).join("udevd")); static_candidates .into_iter() .chain(path_candidates) .find(|path| path.exists()) } fn run_udevd() -> Option> { if let Some(udevd_path) = find_udevd() { let handle = thread::spawn(move || { disable_uevent_helper(); let args: &[&str] = &["--daemon", "--resolve-names=never"]; utils::run_cmd(udevd_path, args); log!("triggering udev coldplug"); utils::run_cmd("udevadm", &["trigger", "--type=subsystems", "--action=add"]); utils::run_cmd("udevadm", &["trigger", "--type=devices", "--action=add"]); log!("waiting for udev to settle"); utils::run_cmd("udevadm", &["settle"]); log!("udev is done"); }); Some(handle) } else { log!("unable to find udevd, skip udev."); None } } fn get_guest_tools_dir() -> Option { Some( env::current_exe() .ok()? .parent()? .parent()? .to_str()? .to_string(), ) } fn _get_network_devices_from_entries(entries: std::fs::ReadDir) -> Vec> { let mut vec = Vec::new(); // .flatten() ignores lines with reading errors for entry in entries.flatten() { let path = entry.path(); if !path.is_dir() { continue; } if let Ok(net_entries) = std::fs::read_dir(path.join("net")) { // .flatten() ignores lines with reading errors if let Some(entry) = net_entries.flatten().next() { if let Some(fname) = entry.path().file_name() { vec.push(Some(fname.to_string_lossy().to_string())); } } } } vec } fn get_network_devices() -> Vec> { let virtio_net_dir = "/sys/bus/virtio/drivers/virtio_net"; loop { match std::fs::read_dir(virtio_net_dir) { Ok(entries) => { return _get_network_devices_from_entries(entries); } Err(_) => { // Wait a bit to make sure virtio-net is properly registered in the system. thread::sleep(Duration::from_secs_f32(0.25)); } } } } fn get_network_handle( network_dev: Option, guest_tools_dir: Option, ) -> Option> { let network_dev_str = network_dev.unwrap(); log!("setting up network device {}", network_dev_str); return Some(thread::spawn(move || { let args = [ "udhcpc", "-i", &network_dev_str, "-n", "-q", "-f", "-s", &format!("{}/virtme-udhcpc-script", guest_tools_dir.unwrap()), ]; utils::run_cmd("busybox", &args); })); } fn setup_network_lo() -> Option> { return Some(thread::spawn(move || { utils::run_cmd("ip", &["link", "set", "dev", "lo", "up"]); })); } fn setup_network() -> Vec>> { let mut vec = vec![setup_network_lo()]; let cmdline = std::fs::read_to_string("/proc/cmdline").unwrap(); if cmdline.contains("virtme.dhcp") { // Make sure all GIDs are allowed to create raw ICMP sockets (this allows to run ping as // regular user). if let Ok(mut file) = OpenOptions::new() .write(true) .open("/proc/sys/net/ipv4/ping_group_range") { let _ = file.write_all("0 2147483647".as_bytes()); } if let Some(guest_tools_dir) = get_guest_tools_dir() { get_network_devices().into_iter().for_each(|network_dev| { vec.push(get_network_handle( network_dev, Some(guest_tools_dir.to_owned()), )); }); } } vec } fn extract_user_script(virtme_script: &str) -> Option { let start_marker = "virtme.exec=`"; let end_marker = '`'; let (_before, remaining) = virtme_script.split_once(start_marker)?; let (encoded_cmd, _after) = remaining.split_once(end_marker)?; String::from_utf8(BASE64.decode(encoded_cmd).ok()?).ok() } fn run_user_script(uid: u32) { if !std::path::Path::new("/dev/virtio-ports/virtme.stdin").exists() || !std::path::Path::new("/dev/virtio-ports/virtme.stdout").exists() || !std::path::Path::new("/dev/virtio-ports/virtme.stderr").exists() || !std::path::Path::new("/dev/virtio-ports/virtme.dev_stdout").exists() || !std::path::Path::new("/dev/virtio-ports/virtme.dev_stderr").exists() { log!("virtme-init: cannot find script I/O ports; make sure virtio-serial is available",); } else { // Re-create stdout/stderr to connect to the virtio-serial ports. let io_files = [ ("/dev/virtio-ports/virtme.ret", "/dev/virtme.ret"), ("/dev/virtio-ports/virtme.dev_stdin", "/dev/stdin"), ("/dev/virtio-ports/virtme.dev_stdout", "/dev/stdout"), ("/dev/virtio-ports/virtme.dev_stderr", "/dev/stderr"), ]; for (src, dst) in io_files.iter() { if !std::path::Path::new(src).exists() { continue; } if std::path::Path::new(dst).exists() { utils::do_unlink(dst); } utils::do_chown(src, uid, None).ok(); utils::do_symlink(src, dst); } // Detach the process from the controlling terminal let open_tty = |path| open(path, OFlag::O_RDWR, Mode::empty()).expect("failed to open console."); let tty_in = open_tty("/dev/virtio-ports/virtme.stdin"); let tty_out = open_tty("/dev/virtio-ports/virtme.stdout"); let tty_err = open_tty("/dev/virtio-ports/virtme.stderr"); // Determine if we need to switch to a different user, or if we can run the script as root. let user = env::var("virtme_user").unwrap_or_else(|_| String::new()); let (cmd, args) = if !user.is_empty() { ("su", vec![user.as_str(), "-c", USER_SCRIPT]) } else { ("/bin/sh", vec![USER_SCRIPT]) }; clear_virtme_envs(); unsafe { let ret = Command::new(cmd) .args(&args) .pre_exec(move || { nix::libc::setsid(); libc::close(libc::STDIN_FILENO); libc::close(libc::STDOUT_FILENO); libc::close(libc::STDERR_FILENO); // Make stdin a controlling tty. let stdin_fd = libc::dup2(tty_in, libc::STDIN_FILENO); nix::libc::ioctl(stdin_fd, libc::TIOCSCTTY, 1); libc::dup2(tty_out, libc::STDOUT_FILENO); libc::dup2(tty_err, libc::STDERR_FILENO); Ok(()) }) .output() .expect("Failed to execute script"); // Channel the return code to the host via /dev/virtme.ret if let Ok(mut file) = OpenOptions::new().write(true).open("/dev/virtme.ret") { // Write the value of output.status.code() to the file if let Some(code) = ret.status.code() { file.write_all(code.to_string().as_bytes()) .expect("Failed to write to file"); } else { // Handle the case where output.status.code() is None file.write_all(b"-1").expect("Failed to write to file"); } } } poweroff(); } } fn create_user_script(cmd: &str) { utils::create_file(USER_SCRIPT, 0o0755, cmd).expect("Failed to create virtme-script file"); } fn setup_user_script(uid: u32) { if let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") { if let Some(cmd) = extract_user_script(&cmdline) { create_user_script(&cmd); if env::var("virtme_graphics").is_err() { run_user_script(uid); } } } } fn setup_root_home() { // Set up a basic environment (unless virtme-ng is running as root on the host) if env::var("virtme_root_user").is_err() { utils::do_mkdir("/run/tmp/roothome"); utils::do_mount("/run/tmp/roothome", "/root", "", libc::MS_BIND as usize, ""); env::set_var("HOME", "/run/tmp/roothome"); } else { env::set_var("HOME", "/root"); } } fn clear_virtme_envs() { // Parameters that start with virtme_* shouldn't pollute the environment. for (key, _) in env::vars() { if key.starts_with("virtme_") { env::remove_var(key); } } } // Redirect a file descriptor to another. fn redirect_fd(src_fd: i32, dst_fd: i32) { unsafe { libc::dup2(src_fd, dst_fd); } } // Redirect stdout/stderr to a new console device. fn redirect_console(consdev: &str) { let file = OpenOptions::new() .write(true) .open(consdev) .expect("Failed to open console device"); let fd = file.into_raw_fd(); let stdout = std::io::stdout(); let handle = stdout.lock(); let stdout_fd = handle.as_raw_fd(); redirect_fd(fd, stdout_fd); let stderr = std::io::stderr(); let handle = stderr.lock(); let stderr_fd = handle.as_raw_fd(); redirect_fd(fd, stderr_fd); } fn configure_terminal(consdev: &str, uid: u32) { // Set proper user ownership on the default console device utils::do_chown(&consdev, uid, None).ok(); // Redirect stdout/stderr to the new console device. redirect_console(&consdev); if let Ok(params) = env::var("virtme_stty_con") { let output = Command::new("stty") .args(params.split_whitespace()) .stdin(std::fs::File::open(consdev).unwrap()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) // Replace the current init process with a shell session. .output(); log!("{}", String::from_utf8_lossy(&output.unwrap().stderr)); } } fn detach_from_terminal(tty_fd: libc::c_int) { // Detach the process from the controlling terminal unsafe { nix::libc::setsid(); libc::close(libc::STDIN_FILENO); libc::close(libc::STDOUT_FILENO); libc::close(libc::STDERR_FILENO); let stdin_fd = libc::dup2(tty_fd, libc::STDIN_FILENO); nix::libc::ioctl(stdin_fd, libc::TIOCSCTTY, 1); libc::dup2(tty_fd, libc::STDOUT_FILENO); libc::dup2(tty_fd, libc::STDERR_FILENO); } } fn run_shell(tty_fd: libc::c_int, args: &[&str]) { unsafe { Command::new("bash") .args(args) .pre_exec(move || { detach_from_terminal(tty_fd); Ok(()) }) .output() .expect("Failed to start shell session"); } } fn run_user_gui(tty_fd: libc::c_int) { // Generate a bare minimum xinitrc let xinitrc = "/run/tmp/.xinitrc"; // Check if we need to start the sound system. let mut pre_exec_cmd: String = String::new(); if let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") { if cmdline.contains("virtme.sound") { if let Some(guest_tools_dir) = get_guest_tools_dir() { pre_exec_cmd = format!("{}/virtme-sound-script", guest_tools_dir); } } } if let Err(err) = utils::create_file( xinitrc, 0o0644, &format!("{}\n/bin/bash {}", pre_exec_cmd, USER_SCRIPT), ) { log!("failed to generate {}: {}", xinitrc, err); return; } // Run graphical app using xinit directly let mut args = vec!["-l", "-c"]; let storage; if let Ok(user) = env::var("virtme_user") { // Try to fix permissions on the virtual consoles, we are starting X // directly here so we may need extra permissions on the tty devices. utils::run_cmd("bash", &["-c", &format!("chown {} /dev/char/*", user)]); // Clean up any previous X11 state. utils::run_cmd("bash", &["-c", &"rm -f /tmp/.X11*/* /tmp/.X11-lock"]); // Start xinit directly. storage = format!("su {} -c 'xinit /run/tmp/.xinitrc'", user); args.push(&storage); } else { args.push("xinit /run/tmp/.xinitrc"); } run_shell(tty_fd, &args); } fn init_xdg_runtime_dir(uid: u32) { // $XDG_RUNTIME_DIR defines the base directory relative to which user-specific non-essential // runtime files and other file objects (such as sockets, named pipes, ...) should be stored. let dir = format!("/run/user/{}", uid); utils::do_mkdir(&dir); utils::do_chown(&dir, uid, None).ok(); env::set_var("XDG_RUNTIME_DIR", dir); } fn run_user_shell(tty_fd: libc::c_int) { let mut args = vec!["-l"]; let storage; if let Ok(user) = env::var("virtme_user") { args.push("-c"); storage = format!("su {}", user); args.push(&storage); } print_logo(); run_shell(tty_fd, &args); } fn run_user_session(consdev: &str, uid: u32) { let flags = OFlag::O_RDWR | OFlag::O_NONBLOCK; let mode = Mode::empty(); let tty_fd = open(consdev, flags, mode).expect("failed to open console"); setup_user_script(uid); if env::var("virtme_graphics").is_ok() { run_user_gui(tty_fd); } else { run_user_shell(tty_fd); } } fn setup_user_session() { let uid = env::var("virtme_user") .ok() .and_then(|user| utils::get_user_id(&user)) .unwrap_or(0); let consdev = match get_active_console() { Some(console) => console, None => { log!("failed to determine console"); let err = Command::new("bash").arg("-l").exec(); log!("failed to exec bash: {}", err); return; } }; configure_terminal(consdev.as_str(), uid); init_xdg_runtime_dir(uid); setup_root_home(); log!("initialization done"); run_user_session(consdev.as_str(), uid); } fn run_sshd() { if let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") { if cmdline.contains("virtme.ssh") { if let Some(guest_tools_dir) = get_guest_tools_dir() { utils::run_cmd(format!("{}/virtme-sshd-script", guest_tools_dir), &[]); } } } } fn run_snapd() { if let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") { if cmdline.contains("virtme.snapd") { // If snapd is present in the system try to start it, to properly support snaps. let snapd_bin = "/usr/lib/snapd/snapd"; if !Path::new(snapd_bin).exists() { return; } let snapd_state = "/var/lib/snapd/state.json"; if !Path::new(snapd_state).exists() { return; } if let Some(guest_tools_dir) = get_guest_tools_dir() { utils::run_cmd(format!("{}/virtme-snapd-script", guest_tools_dir), &[]); } Command::new(snapd_bin) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .ok(); let snapd_apparmor_bin = "/usr/lib/snapd/snapd-apparmor"; if Path::new(snapd_apparmor_bin).exists() { Command::new(snapd_apparmor_bin) .arg("start") .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .output() .ok(); } } } } fn extract_vsock_exec(cmdline: &str) -> Option { let start_marker = "virtme.vsockexec=`"; let end_marker = '`'; let (_before, remaining) = cmdline.split_once(start_marker)?; let (encoded_cmd, _after) = remaining.split_once(end_marker)?; Some(encoded_cmd.to_string()) } fn setup_socat_console() { if let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") { if let Some(exec) = extract_vsock_exec(&cmdline) { thread::spawn(move || { log!("setting up vsock proxy executing {}", exec); let key = "virtme_vsockmount"; if let Ok(path) = env::var(&key) { utils::do_mkdir(&path); utils::do_mount( &key.replace('_', "."), &path, "9p", 0, "version=9p2000.L,trans=virtio,access=any", ); } let from = "VSOCK-LISTEN:1024,reuseaddr,fork"; let to = format!("EXEC:\"{}\",pty,stderr,setsid,sigint,sane,echo=0", exec); let args = vec![from, &to]; utils::run_cmd("socat", &args); }); } } } fn run_misc_services() -> thread::JoinHandle<()> { thread::spawn(|| { symlink_fds(); mount_virtme_initmounts(); fix_packaging_files(); override_system_files(); run_sshd(); run_snapd(); }) } fn print_logo() { let logo = r#" _ _ __ _(_)_ __| |_ _ __ ___ ___ _ __ __ _ \ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ | \ V /| | | | |_| | | | | | __/_____| | | | (_| | \_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ | |___/"#; println!("{}", logo.trim_start_matches('\n')); println!(" kernel version: {}", get_kernel_version(true)); println!(" (CTRL+d to exit)\n"); } fn main() { // Make sure to always run as PID 1. check_init_pid(); // Basic system initialization (order is important here). configure_environment(); configure_hostname(); mount_kernel_filesystems(); mount_cgroupfs(); configure_limits(); mount_virtme_overlays(); mount_sys_filesystems(); mount_kernel_modules(); run_systemd_tmpfiles(); // Service running in the background for later setup_socat_console(); // Service initialization (some services can be parallelized here). let mut handles = vec![run_udevd(), Some(run_misc_services())]; handles.append(&mut setup_network()); // Wait for the completion of the detached services. for handle in handles.into_iter().flatten() { handle.join().unwrap(); } // Start user session (batch or interactive). set_cwd(); setup_user_session(); // Shutdown the system. poweroff(); } virtme-ng-1.35/virtme_ng_init/src/test.rs000066400000000000000000000003661500572323100205060ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0 #[test] fn test_extract_user_script() { let input = "other=stuff virtme.exec=`SGVsbG8K` is=ignored"; assert_eq!( super::extract_user_script(input), Some("Hello\n".to_string()) ); } virtme-ng-1.35/virtme_ng_init/src/utils.rs000066400000000000000000000106461500572323100206710ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0 //! virtme-ng-init: generic helper functions //! //! Author: Andrea Righi use nix::mount::{mount, MsFlags}; use nix::sys::stat::Mode; use nix::unistd::{chown, Gid, Uid}; use std::ffi::{CString, OsStr}; use std::fmt::Arguments; use std::fs::{File, OpenOptions}; use std::io::{self, Write}; use std::os::unix::fs; use std::os::unix::fs::PermissionsExt; use std::process::{Command, Stdio}; use uzers::get_user_by_name; macro_rules! log { ($($arg:tt)*) => { $crate::utils::log_impl(std::format_args!($($arg)*)) }; } pub fn log_impl(msg: Arguments<'_>) { static PREFIX: &str = "<6>virtme-ng-init: "; static LOG_LEVEL: &str = "<6>"; let mut msg = format!("{}{}", PREFIX, msg); // Remove all trailing \n while msg.ends_with('\n') { msg.pop(); } // Was the message empty? If so, do not log anything if PREFIX == msg { return; } match OpenOptions::new().write(true).open("/dev/kmsg") { Ok(mut file) => { msg.push('\n'); file.write_all(msg.as_bytes()).ok(); } Err(_) => { println!( "{}", msg.strip_prefix(LOG_LEVEL) .expect("The message should always start with the log level") ); } } } pub fn get_user_id(username: &str) -> Option { Some(get_user_by_name(username)?.uid()) } pub fn do_chown(path: &str, uid: u32, gid: Option) -> std::io::Result<()> { let gid_option = gid.map(|gid| Gid::from_raw(gid)); chown(path, Some(Uid::from_raw(uid)), gid_option) .map_err(|err| io::Error::new(std::io::ErrorKind::Other, err))?; Ok(()) } pub fn do_mkdir(path: &str) { let dmask = Mode::S_IRWXU | Mode::S_IRGRP | Mode::S_IXGRP | Mode::S_IROTH | Mode::S_IXOTH; nix::unistd::mkdir(path, dmask).ok(); } pub fn do_unlink(path: &str) { match std::fs::remove_file(path) { Ok(_) => (), Err(err) => { log!("failed to unlink file {}: {}", path, err); } } } fn do_touch(path: &str, mode: u32) { fn _do_touch(path: &str, mode: u32) -> std::io::Result<()> { let file = File::create(path)?; let permissions = std::fs::Permissions::from_mode(mode); file.set_permissions(permissions)?; Ok(()) } if let Err(err) = _do_touch(path, mode) { log!("error creating file: {}", err); } } pub fn create_file(fname: &str, mode: u32, content: &str) -> io::Result<()> { do_touch(fname, mode); if !content.is_empty() { let mut file = File::create(fname)?; file.write_all(content.as_bytes())?; } Ok(()) } pub fn do_symlink(src: &str, dst: &str) { match fs::symlink(src, dst) { Ok(_) => (), Err(err) => { log!("failed to create symlink {} -> {}: {}", src, dst, err); } } } pub fn do_mount_check( source: &str, target: &str, fstype: &str, flags: usize, fsdata: &str, ) -> Result<(), nix::Error> { let source_cstr = CString::new(source).expect("CString::new failed"); let fstype_cstr = CString::new(fstype).expect("CString::new failed"); let fsdata_cstr = CString::new(fsdata).expect("CString::new failed"); let result = mount( Some(source_cstr.as_ref()), target, Some(fstype_cstr.as_ref()), MsFlags::from_bits_truncate(flags.try_into().unwrap()), Some(fsdata_cstr.as_ref()), ); result } pub fn do_mount(source: &str, target: &str, fstype: &str, flags: usize, fsdata: &str) { let result = do_mount_check(source, target, fstype, flags, fsdata); if let Err(err) = result { if err != nix::errno::Errno::ENOENT { log!("mount {} -> {}: {}", source, target, err); } } } pub fn run_cmd(cmd: impl AsRef, args: &[&str]) { let output = Command::new(&cmd) .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output(); match output { Ok(output) => { if !output.stderr.is_empty() { log!( "{}", String::from_utf8_lossy(&output.stderr).trim_end_matches('\n') ); } } Err(_) => { log!( "WARNING: failed to run: {:?} {}", cmd.as_ref(), args.join(" ") ); } } } virtme-ng-1.35/vng000077500000000000000000000012541500572323100140720ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- mode: python -*- # vng: The main command-line virtme-ng frontend # This file is not installed; it's just use to run virtme-ng from inside a # source distribution. import os import sys os.environ["__VNG_LOCAL"] = "1" from virtme_ng import run # noqa: E402 # Update PATH to make sure that virtme-ng can be executed directly from the # source directory, without necessarily installing virtme-ng in the system. def update_path(): script_dir = os.path.dirname(os.path.abspath(__file__)) current_path = os.environ.get("PATH", "") new_path = f"{script_dir}:{current_path}" os.environ["PATH"] = new_path update_path() sys.exit(run.main())