pax_global_header00006660000000000000000000000064147357435160014531gustar00rootroot0000000000000052 comment=f564701b57c992a58d9fb5f70d1b1933739ce682 virtme-ng-1.32/000077500000000000000000000000001473574351600133665ustar00rootroot00000000000000virtme-ng-1.32/.github/000077500000000000000000000000001473574351600147265ustar00rootroot00000000000000virtme-ng-1.32/.github/workflows/000077500000000000000000000000001473574351600167635ustar00rootroot00000000000000virtme-ng-1.32/.github/workflows/pylint.yml000066400000000000000000000015551473574351600210330ustar00rootroot00000000000000name: 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.32/.github/workflows/run.yml000066400000000000000000000023021473574351600203070ustar00rootroot00000000000000name: 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.32/.gitignore000066400000000000000000000005601473574351600153570ustar00rootroot00000000000000*~ __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.32/.gitmodules000066400000000000000000000001501473574351600155370ustar00rootroot00000000000000[submodule "virtme_ng_init"] path = virtme_ng_init url = https://github.com/arighi/virtme-ng-init.git virtme-ng-1.32/DCO-1.1.txt000066400000000000000000000023741473574351600150370ustar00rootroot00000000000000The 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.32/LICENSE000066400000000000000000000432541473574351600144030ustar00rootroot00000000000000 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.32/Makefile000066400000000000000000000016031473574351600150260ustar00rootroot00000000000000INSTALL_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_ng_init/src/*.rs: git submodule update --init --recursive 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.32/README.md000066400000000000000000000513761473574351600146610ustar00rootroot00000000000000https://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/ ``` - 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@gpd3~> scp -P 2222 localhost:~/result.txt . ``` - 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, modify the `default_opts` sections 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.32/bin/000077500000000000000000000000001473574351600141365ustar00rootroot00000000000000virtme-ng-1.32/bin/virtme-prep-kdir-mods000077500000000000000000000047141473574351600202330ustar00rootroot00000000000000#!/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 # prepararion work again. if [ -f "$MODDIR/modules.dep" ]; then rm $MODDIR/modules.dep fi # 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.32/cfg/000077500000000000000000000000001473574351600141255ustar00rootroot00000000000000virtme-ng-1.32/cfg/virtme-ng.conf000066400000000000000000000000411473574351600166770ustar00rootroot00000000000000{ "default_opts" : { } } virtme-ng-1.32/pyproject.toml000066400000000000000000000010221473574351600162750ustar00rootroot00000000000000[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\ """, ] virtme-ng-1.32/setup.cfg000066400000000000000000000022031473574351600152040ustar00rootroot00000000000000[flake8] max_line_length = 120 per-file-ignores = # E402 module level import not at top of file annotations: E402 [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.32/setup.py000077500000000000000000000126631473574351600151130ustar00rootroot00000000000000#!/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", "r", 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'] 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) if sys.version_info < (3, 8): print("virtme-ng requires Python 3.8 or higher") sys.exit(1) packages = [ "virtme_ng", "virtme", "virtme.commands", "virtme.guest", ] package_files = [ "virtme-init", "virtme-udhcpc-script", "virtme-sshd-script", "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"), "r", encoding="utf-8" ).read(), long_description_content_type="text/markdown", install_requires=[ 'argcomplete', 'requests', # `pkg_resources` is removed in python 3.12, moved to setuptools. # # TODO: replace pkg_resources with importlib. # pylint: disable=fixme '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", ], 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.32/virtme-configkernel000077500000000000000000000010601473574351600172630ustar00rootroot00000000000000#!/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 exit(configkernel.main()) virtme-ng-1.32/virtme-mkinitramfs000077500000000000000000000010731473574351600171450ustar00rootroot00000000000000#!/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 exit(mkinitramfs.main()) virtme-ng-1.32/virtme-run000077500000000000000000000010361473574351600154240ustar00rootroot00000000000000#!/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 exit(run.main()) virtme-ng-1.32/virtme/000077500000000000000000000000001473574351600146745ustar00rootroot00000000000000virtme-ng-1.32/virtme/__init__.py000066400000000000000000000000001473574351600167730ustar00rootroot00000000000000virtme-ng-1.32/virtme/architectures.py000066400000000000000000000265041473574351600201220ustar00rootroot00000000000000# -*- 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 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 "virtio-%s-pci" % virtiotype @staticmethod def vhost_dev_type() -> str: return "vhost-user-fs-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_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 "arch/%s/boot/bzImage" % self.linuxname def img_name(self) -> 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 = "%s_defconfig" % name @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 "virtio-%s-device" % virtiotype @staticmethod def vhost_dev_type() -> str: return "vhost-user-fs-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 "virtio-%s-device" % virtiotype @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 "virtio-%s-device" % virtiotype @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) -> 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 virtio_dev_type(virtiotype): return "virtio-%s-ccw" % virtiotype @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"]) # 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"] @staticmethod def qemu_serial_console_args(): return ["-device", "sclpconsole,chardev=console"] def img_name(self) -> str: return "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.32/virtme/commands/000077500000000000000000000000001473574351600164755ustar00rootroot00000000000000virtme-ng-1.32/virtme/commands/__init__.py000066400000000000000000000000001473574351600205740ustar00rootroot00000000000000virtme-ng-1.32/virtme/commands/configkernel.py000066400000000000000000000275011473574351600215220ustar00rootroot00000000000000# -*- 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 from typing import Optional import sys import argparse import os import platform import shlex import shutil import subprocess import multiprocessing from .. import architectures from ..util import SilentError 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", 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() custom_conf = [] if args.custom: for conf_chunk in args.custom: with open(conf_chunk, "r", 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}") 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") # Propagate additional Makefile variables for var in args.envs: if var.startswith("O="): # Setting "O=..." takes precedence over KBUILD_OUTPUT. os.environ["KBUILD_OUTPUT"] = var[2:] archargs.append(shlex.quote(var)) # Determine if an initial config is present config = ".config" makef = "Makefile" # Check if KBUILD_OUTPUT is defined and if it's a directory config_dir = os.environ.get("KBUILD_OUTPUT", "") if config_dir and os.path.isdir(config_dir): 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: 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.32/virtme/commands/mkinitramfs.py000077500000000000000000000032551473574351600214030ustar00rootroot00000000000000# -*- 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 sys import argparse from .. import modfinder from .. import virtmods from .. import mkinitramfs 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.32/virtme/commands/run.py000066400000000000000000001710631473574351600176630ustar00rootroot00000000000000# -*- 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 from typing import Any, Optional, List, NoReturn, Dict, Tuple import atexit import argparse import tempfile import os import platform import errno import fcntl import sys import shlex import re import itertools import subprocess import signal import termios from shutil import which from time import sleep from base64 import b64encode from virtme_ng.utils import CACHE_DIR from .. import virtmods from .. import modfinder from .. import mkinitramfs from .. import qemu_helpers from .. import architectures from .. import resources from ..util import SilentError, get_username, find_binary_or_raise 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( "--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 = 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).", ) 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, "r", 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(img_name, path): if not os.path.exists(path): arg_fail( "kernel file %s does not exist, try --build to build the kernel" % path ) if not os.access(path, os.R_OK): arg_fail("unable to access %s (check for read permissions)" % path) try: result = subprocess.run( ["file", path], capture_output=True, text=True, check=False ) for item in result.stdout.split(", "): match = re.search(r"^[vV]ersion (\S{3,})", 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(r"Linux version (\S{3,})", 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: match = re.search(fr"{img_name}-(\S{{3,}})", 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: img_name = arch.img_name() # Try to resolve kimg as a kernel version first, then check if a file # is provided. kimg = "/usr/lib/modules/%s/%s" % (args.kimg, img_name) if not os.path.exists(kimg): kimg = "/boot/%s-%s" % (img_name, args.kimg) if not os.path.exists(kimg): kimg = args.kimg if not os.path.exists(kimg): arg_fail("%s does not exist" % args.kimg) kver = get_kernel_version(img_name, kimg) if kver is None: # Unable to detect kernel version, try to boot without # automatically detecting modules. args.mods = "none" sys.stderr.write( "warning: failed to retrieve kernel version from: " + kimg + " (modules may not work)\n" ) else: 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(None, 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"): # Check if modules.order exists, otherwise fallback to mods=none if 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, "r", 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 = "virtfs%d" % len(qemuargs) vhost_dev_type = arch.vhost_dev_type() 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"]) qemuargs.extend(["-numa", "node,memdev=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 = "virtfs%d" % len(qemuargs) qemuargs.extend( [ "-fsdev", "local,id=%s,path=%s,security_model=%s%s%s" % ( 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", "%s,fsdev=%s,mount_tag=%s" % (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 '"%s"' % 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("invalid argument to %s" % func) name, fn = namefile if "=" in fn or "," in fn: arg_fail("%s filenames cannot contain '=' or ','" % (func)) if "=" in name or "," in name: arg_fail("%s device names cannot contain '=' or ','" % (func)) 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 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): 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("console: '%s' file exists: " % console_script_path + "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("virtme_vsockmount=%s" % console_script_dir) kernelargs.extend([f"virtme.vsockexec=`{console_exec}`"]) qemuargs.extend(["-device", "vhost-vsock-pci,guest-cid=%d" % args.port]) def ssh_client(args): if args.remote_cmd is not None: exec_escaped = args.remote_cmd.replace('"', '\\"') remote_cmd = ["bash", "-c", exec_escaped] else: remote_cmd = [] cmd = ["ssh", "-p", str(args.port), "localhost"] + remote_cmd if args.dry_run: print(" ".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_key_dir = f"{CACHE_DIR}/.ssh" os.makedirs(f"{ssh_key_dir}/etc/ssh", exist_ok=True) os.system(f"ssh-keygen -A -f {ssh_key_dir}") # 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"]) # Tell virtme-ng-init / virtme-init to start sshd and use the current # username keys/credentials. username = get_username() kernelargs.extend(["virtme.ssh"]) kernelargs.extend([f"virtme_ssh_user={username}"]) # Setup a port forward network interface for the guest. qemuargs.extend(["-device", "%s,netdev=ssh" % (arch.virtio_dev_type("net"))]) qemuargs.extend(["-netdev", "user,id=ssh,hostfwd=tcp::%d-:22" % args.port]) # Allowed characters in mount paths. We can extend this over time if needed. _SAFE_PATH_PATTERN = "[a-zA-Z0-9_+ /.-]+" _RWDIR_RE = re.compile("^(%s)(?:=(%s))?$" % (_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("virtme_hostname=%s" % 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', 'r', 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( "virtme_link_mods=/%s" % 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("invalid --%s parameter %r" % (dirtype, dirarg)) 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("%r is not inside the root" % hostpath) # 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 i, d in enumerate(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 = "virtme.initmount%d" % idx virtfs_config = VirtFSConfig( path=hostpath, mount_tag=tag, readonly=(dirtype != "rwdir"), ) export_virtfs(qemu, arch, qemuargs, virtfs_config) kernelargs.append("virtme_initmount%d=%s" % (idx, guestpath)) for i, d in enumerate(args.overlay_rwdir): kernelargs.append("virtme_rw_overlay%d=%s" % (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"]) if args.verbose: # Check if we have permission to access the current stderr. if not can_access_file("/proc/self/fd/2"): print( "ERROR: not a valid pts, try to run vng inside tmux or screen", file=sys.stderr, ) sys.exit(1) # Redirect kernel messages to stderr, creating a separate console 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"]) # 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. kernelargs.extend(arch.earlyconsole_args()) qemuargs.extend(["-chardev", "stdio,id=console,signal=off,mux=on"]) qemuargs.extend(["-serial", "chardev:console"]) 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( [ "virtme_stty_con=rows %d cols %d iutf8" % (terminal_size.lines, terminal_size.columns) ] ) 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=%s" % os.environ["TERM"]]) if args.sound: qemuargs.extend(arch.qemu_sound_args()) kernelargs.extend(["virtme.sound"]) if args.balloon: qemuargs.extend(["-device", "%s,id=balloon0" % 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 = "blk-disk%d" % i name, fn = sanitize_disk_args("--blk-disk", d) qemuargs.extend( [ "-drive", "if=none,id=%s,file=%s" % (driveid, fn), "-device", "%s,drive=%s,serial=%s" % (arch.virtio_dev_type("blk"), driveid, name), ] ) if args.disk: qemuargs.extend(["-device", "%s,id=scsi" % arch.virtio_dev_type("scsi")]) for i, d in enumerate(args.disk): driveid = "disk%d" % i name, fn = sanitize_disk_args("--disk", d) qemuargs.extend( [ "-drive", "if=none,id=%s,file=%s" % (driveid, fn), "-device", "scsi-hd,drive=%s,vendor=virtme,product=disk,serial=%s" % (driveid, 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, 'r', 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 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("/proc/self/fd/%d" % 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("--net-mac-address: invalid MAC address: '%s'" % args.net_mac_address) return ",mac=" + ":".join(mac) if args.net: extend_dhcp = False index = 0 for net in args.net: qemuargs.extend(["-device", "%s,netdev=n%d%s" % (arch.virtio_dev_type("net"), index, get_net_mac(index))]) if net == "user": qemuargs.extend(["-netdev", "user,id=n%d" % 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", "bridge,id=n%d,br=%s" % (index, bridge)]) extend_dhcp = True elif net == "loop": hubid = index qemuargs.extend(["-netdev", "hubport,id=n%d,hubid=%d" % (index, hubid)]) index += 1 qemuargs.extend(["-device", "%s,netdev=n%d%s" % (arch.virtio_dev_type("net"), index, get_net_mac(index))]) qemuargs.extend(["-netdev", "hubport,id=n%d,hubid=%d" % (index, hubid)]) else: arg_fail("--net: invalid choice: '%s' (choose from user, bridge(=
), loop)" % net) 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("virtme_chdir=%s" % 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("virtme_chdir=%s" % rel_cwd) if args.user and args.user != "root": kernelargs.append("virtme_user=%s" % 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") initramfsfd = initramfsfile.fileno() else: initramfsfd, tmpname = tempfile.mkstemp("irfs") os.unlink(tmpname) initramfsfile = os.fdopen(initramfsfd, "r+b") 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 = "/proc/self/fd/%d" % 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 not args.verbose: kernelargs.append("quiet") kernelargs.append("loglevel=0") else: kernelargs.append("debug") # 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]) # 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(" ".join(shlex.quote(a) for a in 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.32/virtme/cpiowriter.py000077500000000000000000000075071473574351600174510ustar00rootroot00000000000000# -*- 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("%08X" % f 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.32/virtme/guest/000077500000000000000000000000001473574351600160235ustar00rootroot00000000000000virtme-ng-1.32/virtme/guest/__init__.py000066400000000000000000000000001473574351600201220ustar00rootroot00000000000000virtme-ng-1.32/virtme/guest/virtme-init000077500000000000000000000375361473574351600202360ustar00rootroot00000000000000#!/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 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 echo ${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 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}" fi log "basic initialization done" ######## The remainder of this script is a very simple init (PID 1) ######## # Does the system use systemd-tmpfiles? tmpfiles=`which systemd-tmpfiles 2>/dev/null` && { log "running systemd-tmpfiles" systemd-tmpfiles --create --boot --exclude-prefix="/dev" --exclude-prefix="/root" } # 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 cat /proc/cmdline |grep -q -E '(^| )SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1($| )'; 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 mkdir -p -m 0755 /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=`which 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 echo "${virtme_user} ALL = (ALL) NOPASSWD: ALL" >> $tmpfile fi chmod 440 "$tmpfile" if [ ! -f "$real_sudoers" ]; then touch "$real_sudoers" fi mount --bind "$tmpfile" "$real_sudoers" if cat /proc/cmdline |grep -q -E '(^| )virtme.dhcp($| )'; then # 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 cat /proc/cmdline |grep -q -E '(^| )virtme.ssh($| )'; then $(dirname $0)/virtme-sshd-script fi if cat /proc/cmdline |grep -q -E '(^| )virtme.snapd($| )'; 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 2>&1 /run/tmp/.virtme-script if [[ ! -n "${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.stdout 2>/dev/virtio-ports/virtme.stderr else setsid bash /run/tmp/.virtme-script /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 [[ ! -n "${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. export XDG_RUNTIME_DIR=/run/user/$(id -u ${virtme_user}) 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 stty ${virtme_stty_con} <"/dev/$consdev" fi if [[ -n "${virtme_graphics}" ]]; then # Check if we need to enable the sound system. if cat /proc/cmdline |grep -q -E '(^| )virtme.sound($| )'; then pre_exec_cmd="$(dirname $0)/virtme-sound-script" else pre_exec_cmd="" fi # 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.32/virtme/guest/virtme-snapd-script000077500000000000000000000003531473574351600216650ustar00rootroot00000000000000#!/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.32/virtme/guest/virtme-sound-script000077500000000000000000000014651473574351600217150ustar00rootroot00000000000000#!/bin/bash if [ -n "$(which pipewire)" ]; then # Start audio system services. pipewire & wireplumber & pipewire-pulse & # Wait for pulseaudio backend to be up and running. for i 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 "$(which pulseaudio)" ]; then echo "WARNING: pulseaudio subsystem not supported yet" exit 1 elif [ -n "$(which 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.32/virtme/guest/virtme-sshd-script000077500000000000000000000024611473574351600215230ustar00rootroot00000000000000#!/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 mkdir -p /run/sshd rm -f /var/run/nologin 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=/run/sshd fi # Generate authorized_keys in the virtme-ng cache directory and add all # user's public keys. CACHE_DIR=${SSH_HOME}/.cache/virtme-ng/.ssh SSH_AUTH_KEYS="${CACHE_DIR}/authorized_keys" cat "${SSH_HOME}"/.ssh/id_*.pub >> "${SSH_AUTH_KEYS}" 2>/dev/null chown "${virtme_ssh_user}" "${SSH_AUTH_KEYS}" 2>/dev/null chmod 600 "${SSH_AUTH_KEYS}" 2>/dev/null # Generate ssh host keys (if they don't exist already). mkdir -p "${CACHE_DIR}/etc/ssh" ssh-keygen -A -f "${CACHE_DIR}" # Generate a minimal sshd config. SSH_CONFIG=/run/sshd/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 EOF # Start sshd. ARGS=(-f "${SSH_CONFIG}") for key in "${CACHE_DIR}"/etc/ssh/ssh_host_*_key; do ARGS+=(-h "${key}") done /usr/sbin/sshd -f "${SSH_CONFIG}" "${ARGS[@]}" virtme-ng-1.32/virtme/guest/virtme-udhcpc-script000077500000000000000000000026331473574351600220310ustar00rootroot00000000000000#!/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 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.32/virtme/mkinitramfs.py000066400000000000000000000116741473574351600176030ustar00rootroot00000000000000# -*- 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 from typing import List, Dict, Optional import io import os import tempfile import shlex from . import cpiowriter from . import 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", ("bin/%s" % 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( "log 'loading %s...'; insmod %s" % (os.path.basename(p), 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.32/virtme/modfinder.py000066400000000000000000000041671473574351600172250ustar00rootroot00000000000000# -*- 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. """ from typing import List import re import subprocess import platform import itertools 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.32/virtme/qemu_helpers.py000066400000000000000000000036211473574351600177410ustar00rootroot00000000000000# -*- 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("qemu-system-%s" % arch) if qemubin is None and arch == platform.machine(): qemubin = shutil.which("qemu-kvm") if qemubin is None: raise ValueError("cannot find qemu for %s" % arch) else: if not os.path.isfile(qemubin): raise ValueError('specified qemu binary "%s" does not exist' % qemubin) if not os.access(qemubin, os.X_OK): raise ValueError( 'specified qemu binary "%s" is not executable' % qemubin ) 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.32/virtme/resources.py000066400000000000000000000024671473574351600172710ustar00rootroot00000000000000# -*- 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 os import shutil import subprocess import pkg_resources def find_guest_tools(): """Return the path of the guest tools installed with the running virtme.""" if pkg_resources.resource_isdir(__name__, "guest"): return pkg_resources.resource_filename(__name__, "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 = pkg_resources.resource_filename(__name__, "../bin/%s" % name) if os.path.isfile(fn): return 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("could not find script %s" % name) def run_script(name, **kwargs) -> None: fn = find_script(name) subprocess.check_call(executable=fn, args=[fn], **kwargs) virtme-ng-1.32/virtme/util.py000066400000000000000000000032521473574351600162250ustar00rootroot00000000000000# -*- 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 from typing import Optional, Sequence import os import shutil import getpass import itertools 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("Could not find %r" % names) return ret virtme-ng-1.32/virtme/virtmods.py000066400000000000000000000022261473574351600171170ustar00rootroot00000000000000# -*- 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.32/virtme_ng/000077500000000000000000000000001473574351600153605ustar00rootroot00000000000000virtme-ng-1.32/virtme_ng/__init__.py000066400000000000000000000000001473574351600174570ustar00rootroot00000000000000virtme-ng-1.32/virtme_ng/mainline.py000066400000000000000000000055341473574351600175350ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: mainline kernel downloader.""" import os import re import sys import subprocess 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.32/virtme_ng/run.py000066400000000000000000001362311473574351600165440ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: main command-line frontend.""" import argparse import re import os import platform import sys import socket import shutil import json import signal import tempfile from subprocess import ( check_call, check_output, Popen, DEVNULL, PIPE, CalledProcessError, ) from select import select from pathlib import Path import argcomplete from virtme.util import SilentError, get_username from virtme_ng.utils import CONF_FILE, spinner_decorator from virtme_ng.mainline import KernelDownloader from virtme_ng.version import VERSION def check_call_cmd(command, quiet=False, dry_run=False): if dry_run: print(" ".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() # Use select to poll for new data in the file descriptors while process.poll() is None: ready_to_read, _, _ = select([stdout_fd, stderr_fd], [], [], 1) for file in ready_to_read: if file == stdout_fd: line = process.stdout.readline().decode() if line and not quiet: sys.stdout.write(line) sys.stdout.flush() if file == stderr_fd: line = process.stderr.readline().decode() if line: sys.stderr.write(line) sys.stderr.flush() # 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( "--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)", ) 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( "--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 = { "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", }, } MAKE_COMMAND = "make LOCALVERSION=-virtme" 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, "r", 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 list(filter(None, cmd.split(" "))) 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 += " --verbose" if not args.force and not args.kconfig: cmd += " --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 += f" --arch {arch}" user_config = str(Path.home()) + "/.config/virtme-ng/kernel.config" if os.path.exists(user_config): cmd += f" --custom {user_config}" if args.config: for conf in args.config: cmd += f" --custom {conf}" if args.configitem: for citem in args.configitem: cmd += f" --configitem {citem}" # Propagate additional Makefile variables for var in args.envs: cmd += f" {var} " if args.verbose: print(f"cmd: {cmd}") check_call_cmd( self._format_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_COMMAND if args.compiler: make_command += f" HOSTCC={args.compiler} CC={args.compiler}" if args.skip_modules: make_command += f" {target}" if cross_compile and cross_arch: make_command += f" CROSS_COMPILE={cross_compile} ARCH={cross_arch}" # Propagate additional Makefile variables for var in args.envs: make_command += f" {var} " if args.build_host is None: # Build the kernel locally check_call_cmd( self._format_cmd(make_command + " -j" + self.cpus), 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): if args.envs: args.exec = " ".join(args.envs) if args.exec is not None: self.virtme_param["exec"] = f'--script-sh "{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}") 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 else: for var in args.envs: if var.startswith("O="): self.virtme_param["kdir"] = "--kdir ./" + var[2:] if self.virtme_param.get("kdir") is None: self.virtme_param["kdir"] = "--kdir ./" 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_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"] = "--remote-cmd '" + args.remote_cmd + "'" 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_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_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 = item.split() for split_item in split_items: append.append("-a " + split_item) if args.debug: append.append("-a nokaslr") self.virtme_param["append"] = " ".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_qemu_opts(self, args): qemu_args = "" if args.qemu_opts is not None: qemu_args += " ".join(args.qemu_opts) if args.debug: # Enable vmcoreinfo (required by drgn memory dumps) qemu_args += "-device vmcoreinfo " # Enable debug mode and QMP (to trigger memory dump via `vng --dump`) qemu_args += "-s -qmp tcp:localhost:3636,server,nowait " if qemu_args != "": self.virtme_param["qemu_opts"] = "--qemu-opts " + qemu_args else: self.virtme_param["qemu_opts"] = "" 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 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_disable_microvm(args) self._get_virtme_disable_kvm(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_qemu(args) self._get_virtme_qemu_opts(args) self._get_virtme_nvgpu(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["disable_microvm"]} ' + f'{self.virtme_param["disable_kvm"]} ' + 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["qemu"]} ' + f'{self.virtme_param["qemu_opts"]} ' + f'{self.virtme_param["nvgpu"]} ' ) 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('{ "execute": "qmp_capabilities" }\r'.encode("utf-8")) 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.32/virtme_ng/spinner.py000066400000000000000000000070661473574351600174210ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng: UI spinner class.""" import sys import time import threading 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.32/virtme_ng/utils.py000066400000000000000000000010741473574351600170740ustar00rootroot00000000000000# -*- 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") 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.32/virtme_ng/version.py000066400000000000000000000035151473574351600174230ustar00rootroot00000000000000# -*- mode: python -*- # Copyright 2023 Andrea Righi """virtme-ng version""" import os from subprocess import check_output, DEVNULL, CalledProcessError import pkg_resources PKG_VERSION = "1.32" def get_package_version(): try: return pkg_resources.get_distribution("virtme-ng").version except pkg_resources.DistributionNotFound: 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( "cd %s && [ -e ../.git ] && git describe --long --dirty" % os.path.dirname(__file__), 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.32/virtme_ng_init/000077500000000000000000000000001473574351600164035ustar00rootroot00000000000000virtme-ng-1.32/vng000077500000000000000000000012531473574351600141070ustar00rootroot00000000000000#!/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())