diff --git a/.gitignore b/.gitignore index f892f21..f0eea18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ config/ backup/ utils/ -utils.old/ \ No newline at end of file +utils.old/ +bin/ +dist/ +main +quickpassthrough +debug.log \ No newline at end of file diff --git a/README.md b/README.md index db2b61a..28d7be7 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,81 @@ # QuickPassthrough A project to simplify setting up GPU passthrough on your Linux host for [QuickEMU](https://github.com/quickemu-project/quickemu)(vfio support not developed yet) and libvirt/virt-manager +![Quickpassthrough Preview GIF](https://github.com/HikariKnight/quickpassthrough/blob/dev/quickpassthrough_preview.gif?raw=true) -You can use it by simply running +You can use it by simply downloading the latest release and run it inside a terminal/shell or by downloading and compiling it yourself with the commands below. +## Build with current dependencies ```bash git clone https://github.com/HikariKnight/quickpassthrough.git cd quickpassthrough -./vfio-setup +go mod download +CGO_ENABLED=0 go build -o quickpassthrough cmd/main.go ``` -## Features +## Build with newest dependencies (may break) +```bash +git clone https://github.com/HikariKnight/quickpassthrough.git +cd quickpassthrough +go mod download +go get -u ./cmd +CGO_ENABLED=0 go build -o quickpassthrough cmd/main.go +``` -* General warning and info about what you will be needing -* Enable and configure vfio modules -* Configure 2nd GPU for GPU Passthrough -* Dump the selected GPU rom (as some cards require a romfile for passthrough to work), however no rom patching support planned. -* Enable and configure the correct kernel modules and load them early (initramfs-tools, dracut and mkinitcpio) -* Configure kernel arguments for systemd-boot (using kernelstub) -* Configure kernel arguments for grub2 +## Does this work on immutable systems? +Currently no, there might be support for ostree (fedora silverblue, kinoite, etc) at a later time. + +## How do I undo the changes? +There is a "backup/" folder generated on the first run that will have a copy of all your files (and their paths) from before we edited anything. +Copy the files back to your system (blank files inside .d/ folders will be used to "undo" any new config files we wrote) and rebuild your initramfs then remove the kernel arguments listed in config/kernel_args from your bootloader (if your system use kernelstub, grubby or you had to manually add them). + +## How do I just disable vfio for 1 boot? +Remove the vfio kernel arguments from your bootloader by pressing E on the boot menu. The kernel arguments added to the bootloader can be found in the config/kernel_args file +NOTE: You can also just remove them from your bootloader permanently and update your bootloader if you want to keep the config files on your system. + +## What this project does NOT do +* Setup or configure your Virtual Machine (that is your job) +* Optimize your Virtual Machine for Passthrough (again this is your job) +* Optimize your host machine for Passthrough or Virtualization (out of this projects scope) +* Setup and configure GPU Passthrough on systems with 1 graphic card (iGPU counts as 1 Graphic Card by itself, so iGPU with another GPU will work) +* Does not configure passthrough of 3D controllers, as it will not work (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. +NOTE: This project is aimed at desktops and headless servers. + +## Features +* Show general warning to user and inform about making a backup and general expectations +* Detect if user has an amd or intel CPU and provide the correct IOMMU kernel args based on that +* Configure your 2nd GPU for GPU Passthrough (1 for host, 1 for VM) +* Use [ls-iommu](https://github.com/HikariKnight/ls-iommu) to find PCI devices like graphic cards, usb controllers, etc and see what IOMMU group they are in +* Enable and configure vfio modules (initramfs-tools, dracut, modprobe and mkinitcpio) +* Generate the correct kernel arguments for grub and systemd-boot +* Generate a script to use for dumping the vbios rom (as some cards require a romfile for passthrough to work), however no rom patching support planned. +* Configure kernel arguments for systemd-boot (using kernelstub or grubby) +* Configure kernel arguments for grub2 (editing /etc/default/grub or using grubby) +* A menu system you can navigate through, built using [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Help appreciated to make this menu better!) +* Make sure [vendor-reset](https://github.com/gnif/vendor-reset) module is loaded before vfio, check the repository for the list of cards that require it! * Provides you with the correct kernel arguments to add to your bootloader entry if a supported bootloader is not found -## Contributing +## Features now handled by [ls-iommu](https://github.com/HikariKnight/ls-iommu) +* Automatically handle GPUs where parts of it might be in separate IOMMU groups (ex: RX6600XT) +* Fetch the ID and PCI Address of devices +* Locate the vbios rom path on the system +* Tell the user to enable IOMMU (VT-d/AMD-v) on their motherboard and bootloader +* Get a list of devices, their IOMMU groups and various other information +## Contributing -I know my bash skills are not great, so help is always welcome! And help is wanted here. -If you know bash well, you will be able to help! Just make a pull request to the [dev branch](https://github.com/HikariKnight/quickpassthrough/tree/dev) with your changes! -Just remember to add comments to document the work and explain it for people who are less familiar with the bash syntax or anything else you use. 😄 +This project originally started out as a bash only project, upon completing the proof of concept it became very clear that bash would become very messy with all the weird quirks and regex and inline editing of files.
+So the project moved over to golang, this lets us utilize TUI toolkits like to build a proper menu system for the project.
+ +If you know golang, passthrough or qemu, you are welcome to help! Just make a pull request to the [dev branch](https://github.com/HikariKnight/quickpassthrough/tree/dev) with your changes!
+Just remember to add comments to document the work and explain it for people who are less familiar with the golang syntax or anything else you use. 😄 Also if you know English, you can help by just proof reading. English is not my native language, plus I have dyslexia so I often make spelling mistakes. Proof reading is still contribution! -## TODO - -* ~~Everything~~ -* ~~Show general warning to user and inform about making a backup and general expectations~~ -* ~~Detect if user has an amd or intel CPU and provide the correct IOMMU kernel args based on that~~ -* ~~Tell user to enable IOMMU (VT-d/AMD-v) on their motherboard and bootloader~~ -* ~~Integrate ls-iommu and locate graphic cards and see what IOMMU group they are in~~ -* ~~Enable and configure vfio modules~~ -* ~~Fetch the ID for the GPUs and generate the correct kernel arguments for grub and systemd-boot~~ -* ~~Dump the GPU rom, just in case it will be needed for passthrough~~ (no rom patching planned due to complexity) -* ~~Get help to actually make the scripts better~~ -* Automatically handle GPUs where parts of it might be in separate IOMMU groups (ex: RX5600XT) -* A non-hacky menu system? (I will need help by some bash wizards for this) -* Coloured highlight/text for important information? -* Install vendor\_reset kernel module? (maybe far future, need to detect if user is using a card that needs it, can however mention where to get it) -* Suggest looking-glass if they are going to run Windows - ---- -### Why bash? +### Why GO? -I wanted the dependencies to be minimal without the need for compilation and to avoid potential breaking changes in the future (like with the transition from python2 to python3). - -I know enough bash to make things work, but I am in no way a professional in writing bash scripts as I usually write python and golang. - -There is also quite a lot of perl usage as I am quite familiar with the perl regex format over something like sed +I wanted to learn it, while also using a language that would potentially not create system dependencies. GO fits this criteria when you compile using CGO_ENABLED=0 as this will statically link the libraries and still produce a fairly small (when compressed with upx) binary. diff --git a/cmd/.goreleaser.yaml b/cmd/.goreleaser.yaml new file mode 100644 index 0000000..f2d4dbc --- /dev/null +++ b/cmd/.goreleaser.yaml @@ -0,0 +1,29 @@ +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + hooks: + post: + - upx "{{ .Path }}" +archives: + - replacements: + linux: Linux + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# modelines, feel free to remove those if you don't want/use them: +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..5973bf6 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,17 @@ +package main + +import ( + internal "github.com/HikariKnight/quickpassthrough/internal" + downloader "github.com/HikariKnight/quickpassthrough/internal/ls_iommu_downloader" + "github.com/HikariKnight/quickpassthrough/internal/params" +) + +func main() { + // Get all our arguments in 1 neat struct + pArg := params.NewParams() + + if !pArg.Flag["gui"] { + downloader.CheckLsIOMMU() + internal.Tui() + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b722624 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module github.com/HikariKnight/quickpassthrough + +go 1.20 + +require ( + github.com/HikariKnight/ls-iommu v0.0.0-20230912061539-899ed0ca3fd5 + github.com/akamensky/argparse v1.4.0 + github.com/cavaliergopher/grab/v3 v3.0.1 + github.com/charmbracelet/bubbles v0.17.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/gookit/color v1.5.4 + github.com/klauspost/cpuid/v2 v2.2.6 + github.com/nexidian/gocliselect v1.0.0 + golang.org/x/term v0.15.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/buger/goterm v1.0.4 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8026e2 --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/HikariKnight/ls-iommu v0.0.0-20230912061539-899ed0ca3fd5 h1:IEH+I+phCvL0fBzQKoWQxK147ZrsqPhLjzx1dETBT9Y= +github.com/HikariKnight/ls-iommu v0.0.0-20230912061539-899ed0ca3fd5/go.mod h1:+yX6+uXNeERvwFtP/gH/dOW/MA+K10Wi6asYcRtDXd8= +github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= +github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/buger/goterm v1.0.3 h1:7V/HeAQHrzPk/U4BvyH2g9u+xbUW9nr4yRPyG59W4fM= +github.com/buger/goterm v1.0.3/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= +github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= +github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= +github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nexidian/gocliselect v1.0.0 h1:BTxqUqqhwc/O3jJrPuvpF359FjQag7EYgwdEF9cYY+w= +github.com/nexidian/gocliselect v1.0.0/go.mod h1:xyHtRO0Au/S+4tsEooDEj5+VZtkk+RU6RRs7q4o5TmI= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/configs/config_bootloaders.go b/internal/configs/config_bootloaders.go new file mode 100644 index 0000000..cf76100 --- /dev/null +++ b/internal/configs/config_bootloaders.go @@ -0,0 +1,247 @@ +package configs + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" + "github.com/klauspost/cpuid/v2" +) + +// This function just adds what bootloader the system has to our config.bootloader value +// Preference is given to kernelstub because it is WAY easier to safely edit compared to grub2 +func getBootloader(config *Config) { + // Check what bootloader handler we are using + // Check for grub2-mkconfig + _, err := command.Run("which", "grub2-mkconfig") + if err == nil { + // Mark bootloader as grub2 + config.Bootloader = "grub2" + } + + // Check for grub2-mkconfig + _, err = command.Run("which", "grub-mkconfig") + if err == nil { + // Mark bootloader as grub2 + config.Bootloader = "grub2" + } + + // Check for grubby (used by fedora) + _, err = command.Run("which", "grubby") + if err == nil { + // Mark it as unknown as i do not support it yet + config.Bootloader = "grubby" + } + + // Check for kernelstub (used by pop os) + _, err = command.Run("which", "kernelstub") + if err == nil { + config.Bootloader = "kernelstub" + } +} + +// This function adds the default kernel arguments we want to the config/cmdline file +// This gives us a file we can read all the kernel arguments this system needs +// in case of an unknown bootloader +func Set_Cmdline(gpu_IDs []string) { + // Get the system info + cpuinfo := cpuid.CPU + + // Get the configs + config := GetConfig() + + // Write the file containing our kernel arguments to feed the bootloader + fileio.AppendContent("iommu=pt", config.Path.CMDLINE) + + // Write the argument based on which cpu the user got + switch cpuinfo.VendorString { + case "AuthenticAMD": + fileio.AppendContent(" amd_iommu=on", config.Path.CMDLINE) + case "GenuineIntel": + fileio.AppendContent(" intel_iommu=on", config.Path.CMDLINE) + } + + // Add the GPU ids for vfio to the kernel arguments + fileio.AppendContent(fmt.Sprintf(" vfio_pci.ids=%s", strings.Join(gpu_IDs, ",")), config.Path.CMDLINE) +} + +// Configures systemd-boot using kernelstub +func Set_KernelStub() string { + // Get the config + config := GetConfig() + + // Get the kernel args + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + // Write to logger + logger.Printf("Running command:\nsudo kernelstub -a \"%s\"\n", kernel_args) + + // Run the command + _, err := command.Run("sudo", "kernelstub", "-a", kernel_args) + errorcheck.ErrorCheck(err, "Error, kernelstub command returned exit code 1") + + // Return what we did + return fmt.Sprintf("Executed: sudo kernelstub -a \"%s\"", kernel_args) +} + +// Configures grub2 and/or systemd-boot using grubby +func Set_Grubby() string { + // Get the config + config := GetConfig() + + // Get the kernel args + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + // Write to logger + logger.Printf("Running command:\nsudo grubby --update-kernel=ALL --args=\"%s\"\n", kernel_args) + + // Run the command + _, err := command.Run("sudo", "grubby", "--update-kernel=ALL", fmt.Sprintf("--args=%s", kernel_args)) + errorcheck.ErrorCheck(err, "Error, grubby command returned exit code 1") + + // Return what we did + return fmt.Sprintf("Executed: sudo grubby --update-kernel=ALL --args=\"%s\"", kernel_args) +} + +func Configure_Grub2() { + // Get the config struct + config := GetConfig() + + // Make the config file path + conffile := fmt.Sprintf("%s/grub", config.Path.DEFAULT) + + // Make sure we start from scratch by deleting any old file + if fileio.FileExist(conffile) { + os.Remove(conffile) + } + + // Make a regex to get the system path instead of the config path + syspath_re := regexp.MustCompile(`^config`) + sysfile := syspath_re.ReplaceAllString(conffile, "") + + // Make a regex to find the LINUX lines + cmdline_default_re := regexp.MustCompile(`^GRUB_CMDLINE_LINUX_DEFAULT=\"(.+)\"$`) + currentargs_re := regexp.MustCompile(`^GRUB_CMDLINE_LINUX(_DEFAULT|)=\"(.?|.+)\"$`) + + // Make a bool so we know if we edited the default line if both are in the template + default_edited := false + + // Read the mkinitcpio file + grub_content := fileio.ReadLines(sysfile) + + // Write to logger + logger.Printf("Read %s:\n%s\n", sysfile, strings.Join(grub_content, "\n")) + + for _, line := range grub_content { + if currentargs_re.MatchString(line) { + // Get the current modules + old_kernel_args := strings.Split(currentargs_re.ReplaceAllString(line, "${2}"), " ") + + // Clean up the old arguments by removing vfio related kernel arguments + new_kernel_args := clean_Grub2_Args(old_kernel_args) + + // Get the kernel args from our config + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + // Add our kernel args to the list + new_kernel_args = append(new_kernel_args, kernel_args) + + // If we are at the line starting with MODULES= + if cmdline_default_re.MatchString(line) { + // Write to logger + logger.Printf("Replacing line in %s:\n%s\nWith:\nGRUB_CMDLINE_LINUX_DEFAULT=\"%s\"\n", conffile, line, strings.Join(new_kernel_args, " ")) + + // Write the modules line we generated + fileio.AppendContent(fmt.Sprintf("GRUB_CMDLINE_LINUX_DEFAULT=\"%s\"\n", strings.Join(new_kernel_args, " ")), conffile) + + // Mark the default line as edited so we can skip the non default line + default_edited = true + } else { + // If we have not edited the GRUB_CMDLINE_LINUX_DEFAULT line + if !default_edited { + // Write to logger + logger.Printf("Replacing line in %s:\n%s\nWith:\nGRUB_CMDLINE_LINUX=\"%s\"\n", conffile, line, strings.Join(new_kernel_args, " ")) + + // Write the modules line we generated + fileio.AppendContent(fmt.Sprintf("GRUB_CMDLINE_LINUX=\"%s\"\n", strings.Join(new_kernel_args, " ")), conffile) + } + } + } else { + // Write the line to the file since it does not match our regex + fileio.AppendContent(fmt.Sprintf("%s\n", line), conffile) + } + } +} + +func clean_Grub2_Args(old_kernel_args []string) []string { + // Make a regex to get the VFIO related kernel arguments removed, if they already existed + vfio_args_re := regexp.MustCompile(`(amd|intel)_iommu=(on|1)|iommu=(pt|on)|vfio_pci.ids=.+|vfio_pci.disable_vga=\d{1}|rd.driver.pre=vfio_pci`) + + // Make a stringlist to keep our new arguments + var clean_kernel_args []string + + // Loop through current kernel_args and add anything that isnt vfio or vendor-reset related + for _, v := range old_kernel_args { + // If what we find is not a vfio module or vendor-reset module + if !vfio_args_re.MatchString(v) { + // Add module to module list + clean_kernel_args = append(clean_kernel_args, v) + } + } + + // Return cleaned up arguments + return clean_kernel_args +} + +// This function copies our config to /etc/default/grub and updates grub +func Set_Grub2() ([]string, error) { + // Get the config + config := GetConfig() + + // Get the conf file + conffile := fmt.Sprintf("%s/grub", config.Path.DEFAULT) + + // Get the sysfile + sysfile_re := regexp.MustCompile(`^config`) + sysfile := sysfile_re.ReplaceAllString(conffile, "") + + // Write to logger + logger.Printf("Executing command:\nsudo cp -v \"%s\" %s", conffile, sysfile) + + // Make our output slice + var output []string + + // Copy files to system + output = append(output, CopyToSystem(conffile, sysfile)) + + // Set a variable for the mkconfig command + mkconfig := "grub-mkconfig" + // Check for grub-mkconfig + _, err := command.Run("which", "grub-mkconfig") + if err == nil { + // Set binary as grub-mkconfig + mkconfig = "grub-mkconfig" + } else { + mkconfig = "grub2-mkconfig" + } + + // Update grub.cfg + if fileio.FileExist("/boot/grub/grub.cfg") { + output = append(output, fmt.Sprintf("Executed: sudo %s -o /boot/grub/grub.cfg\nSee debug.log for more detailed output", mkconfig)) + _, mklog, err := command.RunErr("sudo", mkconfig, "-o", "/boot/grub/grub.cfg") + logger.Printf(strings.Join(mklog, "\n")) + errorcheck.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") + } else { + output = append(output, fmt.Sprintf("Executed: sudo %s -o /boot/grub/grub.cfg\nSee debug.log for more detailed output", mkconfig)) + _, mklog, err := command.RunErr("sudo", mkconfig, "-o", "/boot/grub2/grub.cfg") + logger.Printf(strings.Join(mklog, "\n")) + errorcheck.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") + } + + return output, err +} diff --git a/internal/configs/config_dracut.go b/internal/configs/config_dracut.go new file mode 100644 index 0000000..f31a2ff --- /dev/null +++ b/internal/configs/config_dracut.go @@ -0,0 +1,41 @@ +package configs + +import ( + "fmt" + "os" + "strings" + + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" +) + +// This function writes a dracut configuration file for /etc/dracut.conf.d/ +func Set_Dracut() { + config := GetConfig() + + // Set the dracut config file + dracutConf := fmt.Sprintf("%s/vfio.conf", config.Path.DRACUT) + + // If the file already exists then delete it + if fileio.FileExist(dracutConf) { + os.Remove(dracutConf) + } + + // Write to logger + logger.Printf("Writing to %s:\nadd_drivers+=\" %s \"\n", dracutConf, strings.Join(vfio_modules(), " ")) + + // Write the dracut config file + fileio.AppendContent(fmt.Sprintf("add_drivers+=\" %s \"\n", strings.Join(vfio_modules(), " ")), dracutConf) + + // Get the current kernel arguments we have generated + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + // If the kernel argument is not already in the file + if !strings.Contains(kernel_args, "rd.driver.pre=vfio_pci") { + // Add to our kernel arguments file that vfio_pci should load early (dracut does this using kernel arguments) + fileio.AppendContent(" rd.driver.pre=vfio_pci", config.Path.CMDLINE) + } + + // Make a backup of dracutConf if there is one there + backupFile(strings.Replace(dracutConf, "config", "", 1)) +} diff --git a/internal/configs/config_initramfstools.go b/internal/configs/config_initramfstools.go new file mode 100644 index 0000000..f198959 --- /dev/null +++ b/internal/configs/config_initramfstools.go @@ -0,0 +1,84 @@ +package configs + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" +) + +// Special function to read the header of a file (reads the first N lines) +func initramfs_readHeader(lines int, fileName string) string { + // Open the file + f, err := os.Open(fileName) + errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening %s", fileName)) + defer f.Close() + + header_re := regexp.MustCompile(`^#`) + var header []string + + // Make a new scanner + scanner := bufio.NewScanner(f) + + // Read the first N lines + for i := 0; i < lines; i++ { + scanner.Scan() + if header_re.MatchString(scanner.Text()) { + header = append(header, scanner.Text()) + } + } + + // Return the header + return fmt.Sprintf("%s\n", strings.Join(header, "\n")) +} + +// Reads the system file and copies over the content while inserting the vfio modules +// Takes the config file as argument +func initramfs_addModules(conffile string) { + // Make a regex to get the system path instead of the config path + syspath_re := regexp.MustCompile(`^config`) + + // Make a regex to skip specific modules and comments + skipmodules_re := regexp.MustCompile(`(^#|vendor-reset|vfio|vfio_pci|vfio_iommu_type1|vfio_virqfd)`) + + // Get the syspath + syspath := syspath_re.ReplaceAllString(conffile, "") + + // Open the system file for reading + sysfile, err := os.Open(syspath) + errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening file for reading %s", syspath)) + defer sysfile.Close() + + // Check if user has vendor-reset installed/enabled and make sure that is first + content := fileio.ReadFile(syspath) + if strings.Contains(content, "vendor-reset") { + fileio.AppendContent("vendor-reset\n", conffile) + } + + // Write the vfio modules + fileio.AppendContent( + fmt.Sprint( + "# Added by quickpassthrough #\n", + fmt.Sprintf( + "%s\n", + strings.Join(vfio_modules(), "\n"), + ), + "#############################\n", + ), + conffile, + ) + + // Scan the system file line by line + scanner := bufio.NewScanner(sysfile) + for scanner.Scan() { + // If this is not a line we skip then + if !skipmodules_re.MatchString(scanner.Text()) { + // Add the module to our config + fileio.AppendContent(fmt.Sprintf("%s\n", scanner.Text()), conffile) + } + } +} diff --git a/internal/configs/config_mkinitcpio.go b/internal/configs/config_mkinitcpio.go new file mode 100644 index 0000000..a2f6dd6 --- /dev/null +++ b/internal/configs/config_mkinitcpio.go @@ -0,0 +1,75 @@ +package configs + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" +) + +// This function copies the content of /etc/mkinitcpio.conf to the config folder and does an inline replace/insert on the MODULES=() line +func Set_Mkinitcpio() { + // Get the config struct + config := GetConfig() + + // Make sure we start from scratch by deleting any old file + if fileio.FileExist(config.Path.MKINITCPIO) { + os.Remove(config.Path.MKINITCPIO) + } + + // Make a regex to get the system path instead of the config path + syspath_re := regexp.MustCompile(`^config`) + sysfile := syspath_re.ReplaceAllString(config.Path.MKINITCPIO, "") + + // Make a regex to find the modules line + module_line_re := regexp.MustCompile(`^MODULES=`) + modules_re := regexp.MustCompile(`MODULES=\((.+?)\)`) + vfio_modules_re := regexp.MustCompile(`(vfio_iommu_type1|vfio_pci|vfio_virqfd|vfio|vendor-reset)`) + + // Read the mkinitcpio file + mkinitcpio_content := fileio.ReadLines(sysfile) + + // Write to logger + logger.Printf("Read %s:\n%s\n", sysfile, strings.Join(mkinitcpio_content, "\n")) + + for _, line := range mkinitcpio_content { + // If we are at the line starting with MODULES= + if module_line_re.MatchString(line) { + // Get the current modules + currentmodules := strings.Split(modules_re.ReplaceAllString(line, "${1}"), " ") + + // Get the vfio modules we need to use + modules := vfio_modules() + + // If vendor-reset is in the current modules + if strings.Contains(line, "vendor-reset") { + // Write to logger + logger.Printf("vendor-reset module detected in %s\nMaking sure it will be loaded before vfio\n", sysfile) + + // Add vendor-reset first + modules = append([]string{"vendor-reset"}, modules...) + } + + // Loop through current modules and add anything that isnt vfio or vendor-reset related + for _, v := range currentmodules { + // If what we find is not a vfio module or vendor-reset module + if !vfio_modules_re.MatchString(v) { + // Add module to module list + modules = append(modules, v) + } + } + + // Write to logger + logger.Printf("Replacing line in %s:\n%s\nWith:\nMODULES=(%s)\n", config.Path.MKINITCPIO, line, strings.Join(modules, " ")) + + // Write the modules line we generated + fileio.AppendContent(fmt.Sprintf("MODULES=(%s)\n", strings.Join(modules, " ")), config.Path.MKINITCPIO) + } else { + // Else just write the line to the config + fileio.AppendContent(fmt.Sprintf("%s\n", line), config.Path.MKINITCPIO) + } + } +} diff --git a/internal/configs/config_modprobe.go b/internal/configs/config_modprobe.go new file mode 100644 index 0000000..63a5dbc --- /dev/null +++ b/internal/configs/config_modprobe.go @@ -0,0 +1,66 @@ +package configs + +import ( + "fmt" + "os" + "strings" + + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" +) + +// This function generates a modprobe file for /etc/modprobe.d/ +func Set_Modprobe(gpu_IDs []string) { + // Get the config + config := GetConfig() + + // Read our current kernel arguments + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + var vfio_pci_options []string + vfio_pci_options = append(vfio_pci_options, strings.Join(gpu_IDs, ",")) + + if strings.Contains(kernel_args, "vfio_pci.disable_vga=1") { + // Write to logger + logger.Printf("User has disabled vfio video output on host, adding disable_vga=1 to the optional hardcoded vfio_pci options\n") + vfio_pci_options = append(vfio_pci_options, "disable_vga=1") + } + + // Put our config file path into a string + conffile := fmt.Sprintf("%s/vfio.conf", config.Path.MODPROBE) + + // If the file exists + if fileio.FileExist(conffile) { + // Delete the old file + os.Remove(conffile) + } + + content := fmt.Sprint( + "## This is an autogenerated file that stubs your graphic card for use with vfio\n", + "## This file should be placed inside /etc/modprobe.d/\n", + "# Uncomment the line below to \"hardcode\" your graphic card to be bound to the vfio-pci driver.\n", + "# In most cases this should not be neccessary, it will also prevent you from turning off vfio in the bootloader.\n", + fmt.Sprintf( + "#options vfio_pci ids=%s\n", + strings.Join(vfio_pci_options, " "), + ), + "\n", + "# Make sure vfio_pci is loaded before these modules: nvidia, nouveau, amdgpu and radeon\n", + "softdep nvidia pre: vfio vfio_pci\n", + "softdep nouveau pre: vfio vfio_pci\n", + "softdep amdgpu pre: vfio vfio_pci\n", + "softdep radeon pre: vfio vfio_pci\n", + ) + + // Write to logger + logger.Printf("Writing %s:\n%s", conffile, content) + + // Write the vfio.conf file to our modprobe config + fileio.AppendContent( + content, + conffile, + ) + + // Make a backup of dracutConf if there is one there + backupFile(strings.Replace(conffile, "config", "", 1)) +} diff --git a/internal/configs/config_vbios_dumper.go b/internal/configs/config_vbios_dumper.go new file mode 100644 index 0000000..e8bc92c --- /dev/null +++ b/internal/configs/config_vbios_dumper.go @@ -0,0 +1,70 @@ +package configs + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/logger" +) + +// Generates a script file named dump_vbios.sh and places it inside the utils folder. +// This script has to be run without a display manager or display server running +func GenerateVBIOSDumper(vbios_path string) { + // Get the config directories + config := GetConfig() + + // Get the program directory + exe, _ := os.Executable() + scriptdir := filepath.Dir(exe) + + // If we are using go run use the working directory instead + if strings.Contains(scriptdir, "/tmp/go-build") { + scriptdir, _ = os.Getwd() + } + + vbios_script_template := fmt.Sprint( + "#!/bin/bash\n", + "# THIS FILE IS AUTO GENERATED!\n", + "# IF YOU HAVE CHANGED GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n", + "mkdir -p \"%s\"\n", + "echo Attempting to enable reading from rom\n", + "echo 1 | sudo tee %s\n", + "echo\n", + "echo Attempting to dump VBIOS\n", + "sudo bash -c \"cat %s\" > %s/%s/vfio_card.rom || echo \"\nFailed to dump the VBIOS, in most cases a reboot can fix this.\nOr you have to bind the gpu to the vfio-pci driver, reboot the machine and try dumping again.\nIf that still fails, you might find your VBIOS at: https://www.techpowerup.com/vgabios/\n\"\n", + "file \"%s/%s/vfio_card.rom\"\n", + "echo\n", + "echo Attempting to disable reading from rom \\(cleanup\\)\n", + "echo 0 | sudo tee %s\n", + ) + + vbios_script := fmt.Sprintf( + vbios_script_template, + config.Path.QUICKEMU, + vbios_path, + vbios_path, + scriptdir, + config.Path.QUICKEMU, + scriptdir, + config.Path.QUICKEMU, + vbios_path, + ) + + // Make the script file + scriptfile, err := os.Create("utils/dump_vbios.sh") + errorcheck.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"") + defer scriptfile.Close() + + // Make the script executable + scriptfile.Chmod(0775) + errorcheck.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"") + + // Write to logger + logger.Printf("Writing utils/dump_vbios.sh\n") + + // Write the script + scriptfile.WriteString(vbios_script) +} diff --git a/internal/configs/config_vfio_video.go b/internal/configs/config_vfio_video.go new file mode 100644 index 0000000..63230b1 --- /dev/null +++ b/internal/configs/config_vfio_video.go @@ -0,0 +1,50 @@ +package configs + +import ( + "fmt" + "os" + "strings" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" +) + +// This function adds the disable vfio video output on host option to the config +// The function will use the given int as the value for the option +func DisableVFIOVideo(i int) { + // Get the config + config := GetConfig() + + // Write to logger + logger.Printf("Adding vfio_pci.disable_vga=%v to %s\n", i, config.Path.CMDLINE) + + // Get the current kernel arguments we have generated + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + + // If the kernel argument is already in the file + if strings.Contains(kernel_args, "vfio_pci.disable_vga") { + // Remove the old file + err := os.Remove(config.Path.CMDLINE) + errorcheck.ErrorCheck(err, fmt.Sprintf("Could not rewrite %s", config.Path.CMDLINE)) + + // Enable or disable the VGA based on our given value + if i == 0 { + kernel_args = strings.Replace(kernel_args, "vfio_pci.disable_vga=1", "vfio_pci.disable_vga=0", 1) + + } else { + kernel_args = strings.Replace(kernel_args, "vfio_pci.disable_vga=0", "vfio_pci.disable_vga=1", 1) + } + + // Rewrite the kernel_args file + fileio.AppendContent(kernel_args, config.Path.CMDLINE) + } else { + // Add to the kernel arguments that we want to disable VFIO video output on the host + fileio.AppendContent( + fmt.Sprintf( + " vfio_pci.disable_vga=%v", i, + ), + config.Path.CMDLINE, + ) + } +} diff --git a/internal/configs/configs.go b/internal/configs/configs.go new file mode 100644 index 0000000..a77ed95 --- /dev/null +++ b/internal/configs/configs.go @@ -0,0 +1,251 @@ +package configs + +import ( + "fmt" + "os" + "regexp" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" + "github.com/HikariKnight/quickpassthrough/pkg/uname" + "github.com/klauspost/cpuid/v2" +) + +type Path struct { + CMDLINE string + MODPROBE string + INITRAMFS string + ETCMODULES string + DEFAULT string + QUICKEMU string + DRACUT string + MKINITCPIO string +} + +type Config struct { + Bootloader string + Cpuvendor string + Path *Path + Gpu_Group string + Gpu_IDs []string +} + +// Gets the path to all the config files +func GetConfigPaths() *Path { + Paths := &Path{ + CMDLINE: "config/kernel_args", + MODPROBE: "config/etc/modprobe.d", + INITRAMFS: "config/etc/initramfs-tools", + ETCMODULES: "config/etc/modules", + DEFAULT: "config/etc/default", + QUICKEMU: "config/quickemu", + DRACUT: "config/etc/dracut.conf.d", + MKINITCPIO: "config/etc/mkinitcpio.conf", + } + + return Paths +} + +// Gets all the configs and returns the struct +func GetConfig() *Config { + config := &Config{ + Bootloader: "unknown", + Cpuvendor: cpuid.CPU.VendorString, + Path: GetConfigPaths(), + Gpu_Group: "", + Gpu_IDs: []string{}, + } + + // Detect the bootloader we are using + getBootloader(config) + + return config +} + +// Constructs the empty config files and folders based on what exists on the system +func InitConfigs() { + config := GetConfig() + + // Add all directories we need into a stringlist + dirs := []string{ + config.Path.MODPROBE, + config.Path.INITRAMFS, + config.Path.DEFAULT, + config.Path.DRACUT, + } + + // Remove old config + os.RemoveAll("config") + + // Make the config folder + os.Mkdir("config", os.ModePerm) + + // Make a regex to get the system path instead of the config path + syspath_re := regexp.MustCompile(`^config`) + + // For each directory + for _, confpath := range dirs { + // Get the system path + syspath := syspath_re.ReplaceAllString(confpath, "") + + // If the path exists + if fileio.FileExist(syspath) { + // Write to log + logger.Printf( + "%s found on the system\n"+ + "Creating %s\n", + syspath, + confpath, + ) + + // Make a backup directory + makeBackupDir(syspath) + + // Create the directories for our configs + err := os.MkdirAll(confpath, os.ModePerm) + errorcheck.ErrorCheck(err) + } + } + + // Add all files we need to a stringlist + files := []string{ + config.Path.ETCMODULES, + config.Path.MKINITCPIO, + fmt.Sprintf("%s/modules", config.Path.INITRAMFS), + fmt.Sprintf("%s/grub", config.Path.DEFAULT), + } + + // If we are using grubby + if config.Bootloader == "grubby" { + // Do not create an empty /etc/default/grub file + files = files[:len(files)-1] + } + + for _, conffile := range files { + // Get the system file path + sysfile := syspath_re.ReplaceAllString(conffile, "") + + // If the file exists + if fileio.FileExist(sysfile) { + // Write to log + logger.Printf( + "%s found on the system\n"+ + "Creating %s\n", + sysfile, + conffile, + ) + + // Create the directories for our configs + file, err := os.Create(conffile) + errorcheck.ErrorCheck(err) + // Close the file so we can edit it + file.Close() + + // Backup the sysfile if we do not have a backup + backupFile(sysfile) + } + + // If we now have a config that exists + if fileio.FileExist(conffile) { + switch conffile { + case config.Path.ETCMODULES: + // Write to logger + logger.Printf("Getting the header (if it is there) from %s\n", conffile) + + // Read the header + header := initramfs_readHeader(4, sysfile) + fileio.AppendContent(header, conffile) + + // Add the modules to the config file + initramfs_addModules(conffile) + case fmt.Sprintf("%s/modules", config.Path.INITRAMFS): + // Write to logger + logger.Printf("Getting the header (if it is there) from %s\n", conffile) + + // Read the header + header := initramfs_readHeader(11, sysfile) + fileio.AppendContent(header, conffile) + + // Add the modules to the config file + initramfs_addModules(conffile) + } + } + } +} + +// Returns a list of modules used for vfio based on the systems kernel version +func vfio_modules() []string { + // Make the list of modules + modules := []string{ + "vfio_pci", + "vfio", + "vfio_iommu_type1", + } + + // If we are on a kernel older than 6.2 + sysinfo := uname.New() + kernel_re := regexp.MustCompile(`^(6\.1|6\.0|[1-5]\.)`) + if kernel_re.MatchString(sysinfo.Kernel) { + // Write to the debug log + logger.Printf("Linux kernel version %s detected!\nIncluding vfio_virqfd module\n") + + // Include the vfio_virqfd module + // NOTE: this driver was merged into the vfio module in 6.2 + modules = append(modules, "vfio_virqfd") + } + + // Return the modules + return modules +} + +func backupFile(source string) { + // Make a destination path + dest := fmt.Sprintf("backup%s", source) + + // If the file exists in the config but not on the system it is a file we make + if fileio.FileExist(fmt.Sprintf("config%s", source)) && !fileio.FileExist(source) { + // Create the blank file so that a copy of the backup folder to /etc + file, err := os.Create(dest) + errorcheck.ErrorCheck(err, "Error creating file %s\n", dest) + file.Close() + } else if !fileio.FileExist(dest) { + // If a backup of the file does not exist + // Write to the logger + logger.Printf("No first time backup of %s detected.\nCreating a backup at %s\n", source, dest) + + // Copy the file + fileio.FileCopy(source, dest) + } + +} + +func makeBackupDir(dest string) { + // If a backup directory does not exist + if !fileio.FileExist("backup/") { + // Write to the logger + logger.Printf("Backup directory does not exist!\nCreating backup directory for first run backup") + } + + // Make the empty directories + err := os.MkdirAll(fmt.Sprintf("backup/%s", dest), os.ModePerm) + errorcheck.ErrorCheck(err, "Error making backup/ folder") +} + +// Copy a file to the system, make sure you have run command.Elevate() recently +func CopyToSystem(conffile, sysfile string) string { + // Since we should be elevated with our sudo token we will copy with cp + // (using built in functions will not work as we are running as the normal user) + output, _ := command.Run("sudo", "cp", "-v", conffile, sysfile) + + // Clean the output + clean_re := regexp.MustCompile(`\n`) + clean_output := clean_re.ReplaceAllString(output[0], "") + + // Write output to logger + logger.Printf(clean_output) + + // Return the output + return fmt.Sprintf("Copying: %s", clean_output) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..ebb945f --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,12 @@ +package logger + +import ( + "fmt" + "log" +) + +// Formats our log output to \n%s\n\n for readability +func Printf(content string, v ...any) { + content = fmt.Sprintf("\n%s\n", content) + log.Printf(content, v...) +} diff --git a/internal/ls_iommu_downloader/ls_iommu_downloader.go b/internal/ls_iommu_downloader/ls_iommu_downloader.go new file mode 100644 index 0000000..8becfa5 --- /dev/null +++ b/internal/ls_iommu_downloader/ls_iommu_downloader.go @@ -0,0 +1,192 @@ +package ls_iommu_downloader + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" + "github.com/HikariKnight/quickpassthrough/pkg/untar" + "github.com/cavaliergopher/grab/v3" +) + +// Generated from github API response using https://mholt.github.io/json-to-go/ +type Response struct { + URL string `json:"url"` + AssetsURL string `json:"assets_url"` + UploadURL string `json:"upload_url"` + HTMLURL string `json:"html_url"` + ID int `json:"id"` + Author struct { + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"author"` + NodeID string `json:"node_id"` + TagName string `json:"tag_name"` + TargetCommitish string `json:"target_commitish"` + Name string `json:"name"` + Draft bool `json:"draft"` + Prerelease bool `json:"prerelease"` + CreatedAt time.Time `json:"created_at"` + PublishedAt time.Time `json:"published_at"` + Assets []struct { + URL string `json:"url"` + ID int `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + Label string `json:"label"` + Uploader struct { + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"uploader"` + ContentType string `json:"content_type"` + State string `json:"state"` + Size int `json:"size"` + DownloadCount int `json:"download_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + BrowserDownloadURL string `json:"browser_download_url"` + } `json:"assets"` + TarballURL string `json:"tarball_url"` + ZipballURL string `json:"zipball_url"` + Body string `json:"body"` +} + +func CheckLsIOMMU() { + // Check the API for releases + resp, err := http.Get("https://api.github.com/repos/hikariknight/ls-iommu/releases/latest") + errorcheck.ErrorCheck(err) + + // Close the response when function ends + defer resp.Body.Close() + + // Get the response body + body, err := io.ReadAll(resp.Body) + errorcheck.ErrorCheck(err) + + var result Response + if err := json.Unmarshal(body, &result); err != nil { + fmt.Println("Cant decode JSON") + } + + // Make the directory for ls-iommu if it does not exist + path := "utils" + if !fileio.FileExist(path) { + err := os.Mkdir(path, os.ModePerm) + errorcheck.ErrorCheck(err) + } + + // Generate the download url + downloadUrl := fmt.Sprintf( + "https://github.com/HikariKnight/ls-iommu/releases/download/%s/ls-iommu_Linux_x86_64.tar.gz", + result.TagName, + ) + + // Generate checksums.txt url + checkSumsUrl := fmt.Sprintf( + "https://github.com/HikariKnight/ls-iommu/releases/download/%s/checksums.txt", + result.TagName, + ) + + fileName := fmt.Sprintf("%s/ls-iommu_Linux_x86_64.tar.gz", path) + + // Get the checksum data + checksums, err := http.Get(checkSumsUrl) + errorcheck.ErrorCheck(err) + defer checksums.Body.Close() + checksums_txt, err := io.ReadAll(checksums.Body) + errorcheck.ErrorCheck(err) + + // Check if the tar.gz exists + if !fileio.FileExist(fileName) { + downloadNewVersion(path, fileName, downloadUrl) + if checkSum(string(checksums_txt), fileName) { + err = untar.Untar(fmt.Sprintf("%s/", path), fileName) + errorcheck.ErrorCheck(err) + } + } else { + if !checkSum(string(checksums_txt), fileName) { + downloadNewVersion(path, fileName, downloadUrl) + err = untar.Untar(fmt.Sprintf("%s/", path), fileName) + errorcheck.ErrorCheck(err) + } + } +} + +func checkSum(checksums string, fileName string) bool { + r, err := os.Open(fileName) + errorcheck.ErrorCheck(err) + defer r.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, r); err != nil { + log.Fatal(err) + } + value := hex.EncodeToString(hasher.Sum(nil)) + + return strings.Contains(checksums, value) +} + +func downloadNewVersion(path, fileName, downloadUrl string) { + // Create a request + grabClient := grab.NewClient() + req, _ := grab.NewRequest(fileName, downloadUrl) + + // Remove old archive + os.Remove(fileName) + + // Download ls-iommu + download := grabClient.Do(req) + + // check for errors + if err := download.Err(); err != nil { + fmt.Fprintf(os.Stderr, "Download failed: %v\n", err) + if !fileio.FileExist("utils/ls-iommu") { + log.Fatal("If the above error is 404, then we could not communicate with the GitHub API\n Please manually download and extract ls-iommu to: utils/\nYou can download it from: https://github.com/HikariKnight/ls-iommu/releases") + } else { + fmt.Println("Existing ls-iommu binary detected in \"utils/\", will use that instead as the GitHub API did not respond.") + } + } else { + fmt.Printf("Download saved to ./%v \n", download.Filename) + } +} diff --git a/internal/lsiommu/lsiommu.go b/internal/lsiommu/lsiommu.go new file mode 100644 index 0000000..0574f75 --- /dev/null +++ b/internal/lsiommu/lsiommu.go @@ -0,0 +1,71 @@ +package lsiommu + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/klauspost/cpuid/v2" +) + +func GetIOMMU(args ...string) []string { + var stdout, stderr bytes.Buffer + // Write to logger + logger.Printf("Executing: utils/ls-iommu %s\n", strings.Join(args, " ")) + + // Configure the ls-iommu command + cmd := exec.Command("utils/ls-iommu", args...) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + // Execute the command + err := cmd.Run() + + // Generate the correct iommu string for the system + var iommu_args string + cpuinfo := cpuid.CPU + // Write the argument based on which cpu the user got + switch cpuinfo.VendorString { + case "AuthenticAMD": + iommu_args = "iommu=pt amd_iommu=on" + case "GenuineIntel": + iommu_args = "iommu=pt intel_iommu=on" + } + + // If ls-iommu returns an error then IOMMU is disabled + if err != nil { + fmt.Printf( + "IOMMU disabled in either UEFI/BIOS or in bootloader, or run inside container!\n"+ + "For your bootloader, make sure you have added the kernel arguments:\n"+ + "%s\n", + iommu_args, + ) + os.Exit(1) + } + + // Read the output + var items []string + output, _ := io.ReadAll(&stdout) + + // Write to logger + logger.Printf("ls-iommu query returned\n%s", string(output)) + + // Make regex to shorten vendor names + shortenVendor := regexp.MustCompile(` Corporation:| Technology Inc.:| Electronics Co Ltd:|Advanced Micro Devices, Inc\. \[(AMD)(|\/ATI)\]:`) + + // Parse the output line by line + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + // Write the objects into the list + items = append(items, shortenVendor.ReplaceAllString(scanner.Text(), "${1}")) + } + + // Return our list of items + return items +} diff --git a/internal/pages/01_welcome.go b/internal/pages/01_welcome.go new file mode 100644 index 0000000..8ecafeb --- /dev/null +++ b/internal/pages/01_welcome.go @@ -0,0 +1,50 @@ +package pages + +import ( + "fmt" + "os" + + "github.com/HikariKnight/quickpassthrough/internal/configs" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/gookit/color" +) + +// Welcome page +func Welcome() { + // Clear screen + command.Clear() + + // Write title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("Welcome to Quickpassthrough!") + + // Write welcome message + color.Print( + "This script is meant to make it easier to setup GPU passthrough for\n", + "Qemu based systems. WITH DIFFERENT 2 GPUS ON THE HOST SYSTEM\n", + "However due to the complexity of GPU passthrough\n", + "This script assumes you know how to do (and have done) the following.\n\n", + "* You have already enabled IOMMU, VT-d, SVM and/or AMD-v\n inside your UEFI/BIOS advanced settings.\n", + "* Know how to edit your bootloader\n", + "* Have a bootloader timeout of at least 3 seconds to access the menu\n", + "* Enable & Configure kernel modules\n", + "* Have a backup/snapshot of your system in case the script causes your\n system to be unbootable\n\n", + "By continuing you accept that I am not liable if your system\n", + "becomes unbootable, as you will be asked to verify the files generated\n\n", + "You can press ESC to exit the program at any time.\n\n", + ) + + // Make user accept responsibility + choice := menu.YesNo("Are you sure you want to continue?") + + // If yes, go to next page + if choice == "y" { + configs.InitConfigs() + config := configs.GetConfig() + SelectGPU(config) + } else { + fmt.Println("") + os.Exit(0) + } +} diff --git a/internal/pages/02_select_gpu.go b/internal/pages/02_select_gpu.go new file mode 100644 index 0000000..1b270c5 --- /dev/null +++ b/internal/pages/02_select_gpu.go @@ -0,0 +1,129 @@ +package pages + +import ( + "fmt" + "os" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/configs" + lsiommu "github.com/HikariKnight/quickpassthrough/internal/lsiommu" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/gookit/color" +) + +func SelectGPU(config *configs.Config) { + // Clear the screen + command.Clear() + + // Get the users GPUs + gpus := lsiommu.GetIOMMU("-g", "-F", "vendor:,prod_name,optional_revision:,device_id") + + // Generate a list of choices based on the GPUs and get the users selection + choice := menu.GenIOMMUMenu("Select a GPU to view the IOMMU groups of", gpus) + + // Parse the choice + switch choice { + case "back": + Welcome() + case "": + // If ESC is pressed + fmt.Println("") + os.Exit(0) + default: + config.Gpu_Group = choice + viewGPU(config) + } +} + +func viewGPU(config *configs.Config, ext ...int) { + // Clear the screen + command.Clear() + + // Set mode to relative + mode := "-r" + + // Set mode to relative extended + if len(ext) > 0 { + mode = "-rr" + } + + // Get the IOMMU listings for GPUs + group := lsiommu.GetIOMMU("-g", mode, "-i", config.Gpu_Group, "-F", "vendor:,prod_name,optional_revision:,device_id") + + // Write a title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("This list should only show devices related to your GPU (usually 1 video, 1 audio device)") + + // Print all the gpus + for _, v := range group { + fmt.Println(v) + } + + // Add a new line for tidyness + fmt.Println("") + + // Make an empty string + var choice string + + // Change choices depending on if we have done an extended search or not + if len(ext) > 0 { + choice = menu.YesNoManual("Use this GPU (any extra devices listed may or may not be linked to it) for passthrough?") + } else { + choice = menu.YesNoEXT("Use this GPU (and related devices) for passthrough?") + } + + // Parse the choice + switch choice { + case "": + // If ESC is pressed + fmt.Println("") + os.Exit(0) + + case "ext": + // Run an extended relative search + viewGPU(config, 1) + + case "n": + // Go back to selecting a gpu + SelectGPU(config) + + case "y": + // Get the device ids for the selected gpu using ls-iommu + config.Gpu_IDs = lsiommu.GetIOMMU("-g", mode, "-i", config.Gpu_Group, "--id") + + // If the kernel_args file already exists + if fileio.FileExist(config.Path.CMDLINE) { + // Delete it as we will have to make a new one anyway + err := os.Remove(config.Path.CMDLINE) + errorcheck.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) + } + + // Write initial kernel_arg file + configs.Set_Cmdline(config.Gpu_IDs) + + // Go to the vbios dumper page + genVBIOS_dumper(config) + + case "manual": + config.Gpu_IDs = menu.ManualInput( + "Please manually enter the vendorID:deviceID for every device to use except PCI Express Switches\n"+ + "NOTE: All devices sharing the same IOMMU group will still get pulled into the VM!", + "xxxx:yyyy,xxxx:yyyy,xxxx:yyyy", + ) + + // If the kernel_args file already exists + if fileio.FileExist(config.Path.CMDLINE) { + // Delete it as we will have to make a new one anyway + err := os.Remove(config.Path.CMDLINE) + errorcheck.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) + } + + // Write initial kernel_arg file + configs.Set_Cmdline(config.Gpu_IDs) + + // Go to the vbios dumper page + genVBIOS_dumper(config) + } +} diff --git a/internal/pages/03_vbios_extract.go b/internal/pages/03_vbios_extract.go new file mode 100644 index 0000000..fb79bc2 --- /dev/null +++ b/internal/pages/03_vbios_extract.go @@ -0,0 +1,63 @@ +package pages + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/HikariKnight/quickpassthrough/internal/configs" + lsiommu "github.com/HikariKnight/quickpassthrough/internal/lsiommu" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/gookit/color" +) + +func genVBIOS_dumper(config *configs.Config) { + // Clear the scren + command.Clear() + + // Get the program directory + exe, _ := os.Executable() + scriptdir := filepath.Dir(exe) + + // If we are using go run use the working directory instead + if strings.Contains(scriptdir, "/tmp/go-build") { + scriptdir, _ = os.Getwd() + } + + // Get the vbios path and generate the vbios dumping script + vbios_path := lsiommu.GetIOMMU("-g", "-i", config.Gpu_Group, "--rom")[0] + configs.GenerateVBIOSDumper(vbios_path) + + // Write a title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("Generated \"dump VBIOS\" script") + + // Tell users about the VBIOS dumper script + fmt.Print( + "For some GPUs, you will need to dump the VBIOS and pass the\n", + "rom to the VM along with the card in order to get a functional passthrough.\n", + "In many cases you can find your vbios at https://www.techpowerup.com/vgabios/\n", + "\n", + "You can also attempt to dump your own vbios using the script in\n", + fmt.Sprintf("%s/utils/dump_vbios.sh\n", scriptdir), + "\n", + ) + + // Get the OK press + choice := menu.OkBack("Make sure you run the script with the display-manager stopped using ssh or tty!") + + // Parse choice + switch choice { + case "next": + disableVideo(config) + + case "back": + SelectGPU(config) + + case "": + fmt.Println("") + os.Exit(0) + } +} diff --git a/internal/pages/04_disable_video.go b/internal/pages/04_disable_video.go new file mode 100644 index 0000000..889c79a --- /dev/null +++ b/internal/pages/04_disable_video.go @@ -0,0 +1,51 @@ +package pages + +import ( + "fmt" + "os" + + "github.com/HikariKnight/quickpassthrough/internal/configs" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/gookit/color" +) + +func disableVideo(config *configs.Config) { + // Clear the screen + command.Clear() + + // Write a title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("Do you want to disable video output on the VFIO card in Linux?") + + fmt.Print( + "Disabling video output in Linux for the card you want to use in a VM\n", + "will make it easier to successfully do the passthrough without issues.\n", + "\n", + ) + + // Make the yesno menu + choice := menu.YesNoBack("Do you want to force disable video output in linux on this card?") + + switch choice { + case "y": + // Add disable VFIO video to the config + configs.DisableVFIOVideo(1) + //selectUSB(config) + prepModules(config) + + case "n": + // Do not disable VFIO Video + configs.DisableVFIOVideo(0) + //selectUSB(config) + prepModules(config) + + case "back": + genVBIOS_dumper(config) + + case "": + // If ESC is pressed + fmt.Println("") + os.Exit(0) + } +} diff --git a/internal/pages/05_select_usbctrl.go b/internal/pages/05_select_usbctrl.go new file mode 100644 index 0000000..3264f7a --- /dev/null +++ b/internal/pages/05_select_usbctrl.go @@ -0,0 +1,91 @@ +package pages + +import ( + "fmt" + "os" + + "github.com/HikariKnight/quickpassthrough/internal/configs" + lsiommu "github.com/HikariKnight/quickpassthrough/internal/lsiommu" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/gookit/color" +) + +func selectUSB(config *configs.Config) { + // Clear the screen + command.Clear() + + // Get the users GPUs + usbs := lsiommu.GetIOMMU("-u", "-F", "vendor:,prod_name,optional_revision:,device_id") + + // Generate a list of choices based on the GPUs and get the users selection + choice := menu.GenIOMMUMenu("Select a USB to view the IOMMU groups of", usbs, 1) + + // Parse the choice + switch choice { + case "back": + disableVideo(config) + + case "": + // If ESC is pressed + fmt.Println("") + os.Exit(0) + + default: + // View the selected GPU + viewUSB(choice, config) + } +} + +func viewUSB(id string, config *configs.Config, ext ...int) { + // Clear the screen + command.Clear() + + // Set mode to relative + mode := "-r" + + // Set mode to relative extended + if len(ext) > 0 { + mode = "-rr" + } + + // Get the IOMMU listings for USB controllers + group := lsiommu.GetIOMMU("-u", mode, "-i", id, "-F", "vendor:,prod_name,optional_revision:,device_id") + + // Write a title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("This list should only show the USB controller") + + // Print all the usb controllers + for _, v := range group { + fmt.Println(v) + } + + // Add a new line for tidyness + fmt.Println("") + + // Make an empty string + var choice string + + // Ask if we shall use the devices for passthrough + if len(ext) == 0 { + choice = menu.YesNo("Use all listed devices for passthrough?") + } else { + choice = menu.YesNoEXT("Use all listed devices for passthrough?") + } + + // Parse the choice + switch choice { + case "": + // If ESC is pressed + fmt.Println("") + os.Exit(0) + + case "n": + // Go back to selecting a gpu + selectUSB(config) + + case "y": + // Go to the select a usb controller + } +} diff --git a/internal/pages/06_finalize.go b/internal/pages/06_finalize.go new file mode 100644 index 0000000..95b71c9 --- /dev/null +++ b/internal/pages/06_finalize.go @@ -0,0 +1,214 @@ +package pages + +import ( + "encoding/base64" + "fmt" + "log" + "os" + "os/user" + "strings" + "syscall" + + "github.com/HikariKnight/quickpassthrough/internal/configs" + "github.com/HikariKnight/quickpassthrough/internal/logger" + "github.com/HikariKnight/quickpassthrough/pkg/command" + "github.com/HikariKnight/quickpassthrough/pkg/fileio" + "github.com/HikariKnight/quickpassthrough/pkg/menu" + "github.com/HikariKnight/quickpassthrough/pkg/uname" + "github.com/gookit/color" + "golang.org/x/term" +) + +func prepModules(config *configs.Config) { + // If we have files for modprobe + if fileio.FileExist(config.Path.MODPROBE) { + // Configure modprobe + configs.Set_Modprobe(config.Gpu_IDs) + } + + // If we have a folder for dracut + if fileio.FileExist(config.Path.DRACUT) { + // Configure dracut + configs.Set_Dracut() + } + + // If we have a mkinitcpio.conf file + if fileio.FileExist(config.Path.MKINITCPIO) { + configs.Set_Mkinitcpio() + } + + // Configure grub2 here as we can make the config without sudo + if config.Bootloader == "grub2" { + // Write to logger + logger.Printf("Configuring grub2 manually") + configs.Configure_Grub2() + } + + // Finalize changes + finalize(config) +} + +func finalize(config *configs.Config) { + // Clear the screen + command.Clear() + + // Write a title + title := color.New(color.BgHiBlue, color.White, color.Bold) + title.Println("Finalizing configuration") + + color.Print( + "The configuration files have been generated and are\n", + "located inside the \"config\" folder\n", + "\n", + "* The \"kernel_args\" file contains kernel arguments that your bootloader needs\n", + //"* The \"quickemu\" folder contains files that might be\n useable for quickemu in the future\n", + "* The files inside the \"etc\" folder must be copied to your system.\n", + " NOTE: Verify that these files are correctly formated/edited!\n", + "* Once all files have been copied, you need to update your bootloader and rebuild\n", + " your initramfs using the tools to do so by your system.\n", + "\n", + "This program can do this for you, however the program will have to\n", + "type your password to sudo using STDIN, to avoid using STDIN press CTRL+C\n", + "and copy the files, update your bootloader and rebuild your initramfs manually.\n", + "If you want to go back and change something, choose Back\n", + "\nNOTE: A backup of the original files from the first run can be found in the backup folder\n", + ) + + // Make a choice of going next or back + choice := menu.Next("Press Next to continue with sudo using STDIN, ESC to exit or Back to go back.") + + // Parse the choice + switch choice { + case "next": + installPassthrough(config) + + case "back": + // Go back + disableVideo(config) + } + +} + +func installPassthrough(config *configs.Config) { + // Get the user data + user, err := user.Current() + if err != nil { + log.Fatalf(err.Error()) + } + + // Provide a password prompt + fmt.Printf("[sudo] password for %s: ", user.Username) + bytep, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + os.Exit(1) + } + fmt.Print("\n") + + // Elevate with sudo + command.Elevate( + base64.StdEncoding.EncodeToString( + bytep, + ), + ) + + // Make an output string + var output string + + // Based on the bootloader, setup the configuration + if config.Bootloader == "kernelstub" { + // Write to logger + logger.Printf("Configuring systemd-boot using kernelstub") + + // Configure kernelstub + output = configs.Set_KernelStub() + fmt.Printf("%s\n", output) + + } else if config.Bootloader == "grubby" { + // Write to logger + logger.Printf("Configuring bootloader using grubby") + + // Configure kernelstub + output = configs.Set_Grubby() + fmt.Printf("%s\n", output) + + } else if config.Bootloader == "grub2" { + // Write to logger + logger.Printf("Applying grub2 changes") + grub_output, _ := configs.Set_Grub2() + fmt.Printf("%s\n", strings.Join(grub_output, "\n")) + + } else { + kernel_args := fileio.ReadFile(config.Path.CMDLINE) + logger.Printf("Unsupported bootloader, please add the below line to your bootloaders kernel arguments\n%s", kernel_args) + } + + // A lot of linux systems support modprobe along with their own module system + // So copy the modprobe files if we have them + modprobeFile := fmt.Sprintf("%s/vfio.conf", config.Path.MODPROBE) + if fileio.FileExist(modprobeFile) { + // Copy initramfs-tools module to system + output = configs.CopyToSystem(modprobeFile, "/etc/modprobe.d/vfio.conf") + fmt.Printf("%s\n", output) + } + + // Copy the config files for the system we have + initramfsFile := fmt.Sprintf("%s/modules", config.Path.INITRAMFS) + dracutFile := fmt.Sprintf("%s/vfio.conf", config.Path.DRACUT) + if fileio.FileExist(initramfsFile) { + // Copy initramfs-tools module to system + output = configs.CopyToSystem(initramfsFile, "/etc/initramfs-tools/modules") + fmt.Printf("%s\n", output) + + // Copy the modules file to /etc/modules + output = configs.CopyToSystem(config.Path.ETCMODULES, "/etc/modules") + fmt.Printf("%s\n", output) + + // Write to logger + logger.Printf("Executing: sudo update-initramfs -u") + + // Update initramfs + fmt.Println("Executed: sudo update-initramfs -u\nSee debug.log for detailed output") + cmd_out, cmd_err, _ := command.RunErr("sudo", "update-initramfs", "-u") + + cmd_out = append(cmd_out, cmd_err...) + + // Write to logger + logger.Printf(strings.Join(cmd_out, "\n")) + } else if fileio.FileExist(dracutFile) { + // Copy dracut config to /etc/dracut.conf.d/vfio + output = configs.CopyToSystem(dracutFile, "/etc/dracut.conf.d/vfio") + fmt.Printf("%s\n", output) + + // Get systeminfo + sysinfo := uname.New() + + // Write to logger + logger.Printf("Executing: sudo dracut -f -v --kver %s", sysinfo.Release) + + // Update initramfs + fmt.Printf("Executed: sudo dracut -f -v --kver %s\nSee debug.log for detailed output", sysinfo.Release) + _, cmd_err, _ := command.RunErr("sudo", "dracut", "-f", "-v", "--kver", sysinfo.Release) + + // Write to logger + logger.Printf(strings.Join(cmd_err, "\n")) + } else if fileio.FileExist(config.Path.MKINITCPIO) { + // Copy dracut config to /etc/dracut.conf.d/vfio + output = configs.CopyToSystem(config.Path.MKINITCPIO, "/etc/mkinitcpio.conf") + fmt.Printf("%s\n", output) + + // Write to logger + logger.Printf("Executing: sudo mkinitcpio -P") + + // Update initramfs + fmt.Println("Executed: sudo mkinitcpio -P\nSee debug.log for detailed output") + cmd_out, cmd_err, _ := command.RunErr("sudo", "mkinitcpio", "-P") + + cmd_out = append(cmd_out, cmd_err...) + + // Write to logger + logger.Printf(strings.Join(cmd_out, "\n")) + } + + // Make sure prompt end up on next line + fmt.Print("\n") +} diff --git a/internal/params/params.go b/internal/params/params.go new file mode 100644 index 0000000..dab85bb --- /dev/null +++ b/internal/params/params.go @@ -0,0 +1,91 @@ +package params + +import ( + "fmt" + "os" + + "github.com/akamensky/argparse" +) + +/* + The whole purpose of this module is to make a struct + to just carry all our parsed arguments around between functions + + Create a Params struct with all the argparse arguments + pArg := params.NewParams() +*/ + +type Params struct { + Flag map[string]bool + FlagCounter map[string]int + IntList map[string][]int + StringList map[string][]string + String map[string]string +} + +func (p *Params) addFlag(name string, flag bool) { + p.Flag[name] = flag +} + +func (p *Params) addFlagCounter(name string, flag int) { + p.FlagCounter[name] = flag +} + +func (p *Params) addIntList(name string, flag []int) { + p.IntList[name] = flag +} + +func (p *Params) addStringList(name string, flag []string) { + p.StringList[name] = flag +} + +func (p *Params) addString(name string, flag string) { + p.String[name] = flag +} + +func NewParams() *Params { + // Setup the parser for arguments + parser := argparse.NewParser("quickpassthrough", "A utility to help you configure your host for GPU Passthrough") + + // Configure arguments + gui := parser.Flag("g", "gui", &argparse.Options{ + Required: false, + Help: "Launch GUI (placeholder for now)", + }) + + // Parse arguments + err := parser.Parse(os.Args) + if err != nil { + // In case of error print error and print usage + // This can also be done by passing -h or --help flags + fmt.Print(parser.Usage(err)) + os.Exit(4) + } + + // Make our struct + pArg := &Params{ + Flag: make(map[string]bool), + FlagCounter: make(map[string]int), + IntList: make(map[string][]int), + StringList: make(map[string][]string), + String: make(map[string]string), + } + + // Add all parsed arguments to a struct for portability since we will use them all over the program + pArg.addFlag("gui", *gui) + /*pArg.addFlag("gpu", *gpu) + pArg.addFlag("usb", *usb) + pArg.addFlag("nic", *nic) + pArg.addFlag("sata", *sata) + pArg.addFlagCounter("related", *related) + pArg.addStringList("ignore", *ignore) + pArg.addIntList("iommu_group", *iommu_group) + pArg.addFlag("kernelmodules", *kernelmodules) + pArg.addFlag("legacyoutput", *legacyoutput) + pArg.addFlag("id", *id) + pArg.addFlag("pciaddr", *pciaddr) + pArg.addFlag("rom", *rom) + pArg.addString("format", *format)*/ + + return pArg +} diff --git a/internal/ui_main.go b/internal/ui_main.go new file mode 100644 index 0000000..9b0db17 --- /dev/null +++ b/internal/ui_main.go @@ -0,0 +1,24 @@ +package internal + +// A simple example demonstrating the use of multiple text input components +// from the Bubbles component library. + +import ( + "os" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/pages" + tea "github.com/charmbracelet/bubbletea" +) + +// This is where we build everything +func Tui() { + // Log all errors to a new logfile (super useful feature of BubbleTea!) + os.Remove("debug.log") + logfile, err := tea.LogToFile("debug.log", "") + errorcheck.ErrorCheck(err, "Error creating log file") + defer logfile.Close() + + // New WIP Tui + pages.Welcome() +} diff --git a/lib/apply_CHANGES.sh b/lib/apply_CHANGES.sh deleted file mode 100755 index 7e6f175..0000000 --- a/lib/apply_CHANGES.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function make_BACKUP () { - local BACKUPDIR - BACKUPDIR="$SCRIPTDIR/backup" - - if [ ! -d "$BACKUPDIR" ]; - then - # Make the backup directories and backup the files - if [ -d "/etc/initramfs-tools" ]; - then - mkdir -p "$BACKUPDIR/etc/initramfs-tools" - cp -v "/etc/initramfs-tools/modules" "$BACKUPDIR/etc/initramfs-tools/modules" - cp -v "/etc/modules" "$BACKUPDIR/etc/modules" - - elif [ -d "/etc/dracut.conf" ]; - then - mkdir -p "$BACKUPDIR/etc/dracut.conf.d" - if [ -f "/etc/dracut.conf.d/10-vfio.conf" ]; - then - cp -v "/etc/dracut.conf.d/10-vfio.conf" "$BACKUPDIR/etc/dracut.conf.d/10-vfio.conf" - - fi - - elif [ -f "/etc/mkinitcpio.conf" ]; - then - mkdir -p "$BACKUPDIR/etc" - cp -v "/etc/mkinitcpio.conf" "$BACKUPDIR/etc/mkinitcpio.conf" - - fi - - if [ -f "/etc/default/grub" ]; - then - mkdir -p "$BACKUPDIR/etc/default" - cp -v "/etc/default/grub" "$BACKUPDIR/etc/default/grub" - - fi - - if [ -d "/etc/modprobe.d" ]; - then - mkdir -p "$BACKUPDIR/etc/modprobe.d" - - # If a vfio.conf file exists, backup that too - if [ -f "/etc/modprobe.d/vfio.conf" ]; - then - cp -v "/etc/modprobe.d/vfio.conf" "$BACKUPDIR/etc/modprobe.d/vfio.conf" - - fi - - fi - - printf "Backup completed!\n" - - else - echo " -A backup already exists! -backup skipped. -" - fi -} - -function copy_FILES () { - echo "Starting copying files to the system!" - - if [ -d "/etc/modprobe.d" ]; - then - sudo cp -v "$SCRIPTDIR/$MODPROBE/vfio.conf" "/etc/modprobe.d/vfio.conf" - - fi - - if [ -d "/etc/initramfs-tools" ]; - then - sudo cp -v "$SCRIPTDIR/$ETCMODULES" "/etc/modules" - sudo cp -v "$SCRIPTDIR/$INITRAMFS/modules" "/etc/initramfs-tools/modules" - echo " -Rebuilding initramfs" - sudo update-initramfs -u - - elif [ -f "/etc/dracut.conf" ]; - then - cp -v "$SCRIPTDIR/$DRACUT/10-vfio.conf" "/etc/dracut.conf.d/10-vfio.conf" - echo " -Rebuilding initramfs" - sudo dracut -f -v --kver "$(uname -r)" - - elif [ -f "/etc/mkinitcpio.conf" ]; - then - cp -v "$SCRIPTDIR/$MKINITCPIO" "/etc/mkinitcpio.conf" - echo " -Rebuilding initramfs" - sudo mkinitcpio -P - - else - echo " -Unsupported initramfs infrastructure -In order to make vfio work, please add these modules to your -initramfs and make them load early, then rebuild initramfs. - -vfio -vfio_iommu_type1 -vfio_pci -vfio_virqfd - - -Press ENTER to continue once you have done the above." - read -r - - fi - - -} - -function apply_CHANGES () { - clear - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - echo "Configuration is now complete and these files have been generated for your system: -$SCRIPTDIR/$ETCMODULES -$SCRIPTDIR/$INITRAMFS/modules -$SCRIPTDIR/$MODPROBE/vfio.conf - -By proceeding, a backup of your system's version of these files will be placed in -$SCRIPTDIR/backup -unless a backup already exist. - -Then the files above will be copied to your system followed by running followed by updating your -initramfs and then attempt adding new kernel arguments to your bootloader." - - read -r -p "Do you want to proceed with the installation of the files? (no=quit) [Y/n]: " YESNO - - case "${YESNO}" in - [Nn]*) - exit 1 - ;; - *) - make_BACKUP - copy_FILES - exec "$SCRIPTDIR/lib/set_CMDLINE.sh" - ;; - esac -} - - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - apply_CHANGES -} - -main diff --git a/lib/get_GPU.sh b/lib/get_GPU.sh deleted file mode 100755 index e806f4e..0000000 --- a/lib/get_GPU.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -function get_GPU () { - clear - printf "These are your graphic cards, they have to be in separate groups. -The graphic card you want to passthrough cannot be in a group with other devices that -does not belong to itself. Both cards must also have unique hardware ids [xxxx:yyyy]!: - -" - echo "#------------------------------------------#" - "$SCRIPTDIR/utils/ls-iommu" -g -F name,device_id,optional_revision - echo "#------------------------------------------#" - - printf " -Press q to quit -" - - read -r -p "Which group number do you want to check?: " IOMMU_GROUP - - case "${IOMMU_GROUP}" in - [1-9]*) - exec "$SCRIPTDIR/lib/get_GPU_GROUP.sh" "$IOMMU_GROUP" - ;; - [Qq]*) - echo "Aborted, your setup is incomplete! -DO NOT use any of the files from $SCRIPTDIR/config ! -" - ;; - *) - exec "$SCRIPTDIR/lib/get_GPU.sh" - ;; - esac -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - get_GPU -} - -main \ No newline at end of file diff --git a/lib/get_GPU_GROUP.sh b/lib/get_GPU_GROUP.sh deleted file mode 100755 index 4f0042c..0000000 --- a/lib/get_GPU_GROUP.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function get_GPU_GROUP () { - clear - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - printf "For this card to be passthrough-able, it must contain only: -* The GPU/Graphic card -* The GPU Audio Controller - -Optionally it may also include: -* GPU USB Host Controller -* GPU Serial Port -* GPU USB Type-C UCSI Controller -* PCI Bridge (if they are in their own IOMMU groups) - -NOTE: If the audio controller is missing, it might be in a separate IOMMU group, please run -\"%s/utils/ls-iommu\" -i %s -rr -F \"pciaddr,subclass_name:,name,device_id,optional_revision\" -and add the PCI address and deviceID to the config manually (this is not done automatically because the -rr flag is unreliable if the system has 2 cards from the same vendor) - -" "$SCRIPTDIR" "$1" - echo "#------------------------------------------#" - "$SCRIPTDIR/utils/ls-iommu" -i "$1" -r -F subclass_name:,name,device_id,optional_revision # | cut -d " " -f 1-5,6- | perl -pe "s/\[[0-9a-f]{4}\]: //" - echo "#------------------------------------------#" - - printf " -To use any of these devices for passthrough ALL of them (except PCI bridges in their own IOMMU groups) has to be passed through to the VMs\ - -To return to the previous page just press ENTER without typing in anything. -" - read -r -p "Do you want to use these devices for passthrough? [y/N]: " YESNO - - case "${YESNO}" in - [Yy]*) - # Get the hardware ids from the selected group - local GPU_DEVID - GPU_DEVID=$("$SCRIPTDIR/utils/ls-iommu" -i "$1" -r --id | perl -pe "s/\n/,/" | perl -pe "s/,$/\n/") - - # Get the PCI ids - local PCI_ID - PCI_ID=$("$SCRIPTDIR/utils/ls-iommu" -i "$1" -r --pciaddr | perl -pe "s/([0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]{1})\n/\"\1\" /" | perl -pe "s/\s$//") - - # Write the GPU_PCI_IDs to the config that quickemu might make use of in the future - echo "GPU_PCI_ID=($PCI_ID) -USB_CTL_ID=()" > "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - - # Get the GPU ROM - "$SCRIPTDIR/lib/get_GPU_ROM.sh" "$1" - - # Start setting up modules - if [ -d "/etc/initramfs-tools" ]; - then - exec "$SCRIPTDIR/lib/set_INITRAMFSTOOLS.sh" "$GPU_DEVID" - - elif [ -d "/etc/dracut.conf" ]; - then - exec "$SCRIPTDIR/lib/set_DRACUT.sh" "$GPU_DEVID" - - elif [ -f "/etc/mkinitcpio.conf" ]; - then - exec "$SCRIPTDIR/lib/set_MKINITCPIO.sh" "$GPU_DEVID" - else - # Bind GPU to VFIO - "$SCRIPTDIR/lib/set_VFIO.sh" "$GPU_DEVID" - - # Configure modprobe - "$SCRIPTDIR/lib/set_MODPROBE.sh" "$GPU_DEVID" - fi - ;; - *) - exec "$SCRIPTDIR/lib/get_GPU.sh" - ;; - esac -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - get_GPU_GROUP "$1" -} - -main "$1" diff --git a/lib/get_GPU_ROM.sh b/lib/get_GPU_ROM.sh deleted file mode 100755 index 9b7efc1..0000000 --- a/lib/get_GPU_ROM.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091,SC2024 - -function get_GPU_ROM () { - clear - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - VBIOS_PATH=$("$SCRIPTDIR/utils/ls-iommu" -g -i "$1" --rom) - echo "We will now attempt to dump the vbios of your selected GPU. -Passing a VBIOS rom to the card used for passthrough is required for some cards, but not all. -Some cards also requires you to patch your VBIOS romfile, check online if this is neccessary for your card. -The VBIOS will be read from $VBIOS_PATH -This process will require the use of sudo and will run the following commands: - -echo 1 | sudo tee $VBIOS_PATH -sudo cat $VBIOS_PATH > $SCRIPTDIR/$QUICKEMU/vfio_card.rom -echo 0 | sudo tee $VBIOS_PATH - -" - read -r -p "Do you want to dump the VBIOS, choosing N will skip this step [y/N]: " YESNO - case "${YESNO}" in - [Yy]*) - echo 1 | sudo tee "$VBIOS_PATH" - sudo cat "$VBIOS_PATH" > "$SCRIPTDIR/$QUICKEMU/vfio_card.rom" - sudo md5sum "$VBIOS_PATH" | cut -d " " -f 1 > "$SCRIPTDIR/$QUICKEMU/vfio_card.rom.md5" - local ROM_MD5 - ROM_MD5=$(sudo md5sum "$VBIOS_PATH" | cut -d " " -f 1) - echo 0 | sudo tee "$VBIOS_PATH" - local ROMFILE_MD5 - ROMFILE_MD5=$(md5sum "$SCRIPTDIR/$QUICKEMU/vfio_card.rom" | cut -d " " -f 1) - - if [ -f "$SCRIPTDIR/$QUICKEMU/vfio_card.rom" ]; - then - if [ "$ROM_MD5" == "$ROMFILE_MD5" ]; - then - echo "Checksums match!" - echo "Dumping of VBIOS successful!" - echo 'GPU_ROMFILE="vfio_card.rom"' >> "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - - read -r -p "Press ENTER to continue." - else - echo "Checksums does not match!" - echo "Dumping of VBIOS failed, skipping romfile" - mv "$SCRIPTDIR/$QUICKEMU/vfio_card.rom" "$SCRIPTDIR/$QUICKEMU/vfio_card.rom.fail" - echo 'GPU_ROMFILE=""' >> "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - - read -r -p "Press ENTER to continue." - fi - else - echo 'GPU_ROMFILE=""' >> "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - fi - ;; - [Nn]*) - echo 'GPU_ROMFILE=""' >> "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - ;; - *) - echo 'GPU_ROMFILE=""' >> "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - ;; - esac -} - - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - get_GPU_ROM "$1" -} - -main "$1" \ No newline at end of file diff --git a/lib/get_USB_CTL.sh b/lib/get_USB_CTL.sh deleted file mode 100755 index 01567fe..0000000 --- a/lib/get_USB_CTL.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -function get_USB_CTL () { - clear - printf "THIS STEP IS OPTIONAL IF YOU DO NOT PLAN TO USE ANYTHING OTHER THAN MOUSE AND KEYBOARD! -The USB Controller you want to passthrough cannot be in a group with other devices. -Passing through a whole USB Controller (a set of hardwired 1-4 usb ports on the motherboard) -is only needed if you intend to use other devices than just mouse and keyboard with the VFIO enabled VM. - -" - echo "#------------------------------------------#" - "$SCRIPTDIR/utils/ls-iommu" -u -F name,device_id,optional_revision - echo "#------------------------------------------#" - printf " -Press q to quit -" - - read -r -p "Which group number do you want to check?: " IOMMU_GROUP - - case "${IOMMU_GROUP}" in - [1-9]*) - exec "$SCRIPTDIR/lib/get_USB_CTL_GROUP.sh" "$IOMMU_GROUP" - ;; - [Qq]*) - exec "$SCRIPTDIR/lib/apply_CHANGES.sh" - ;; - *) - exec "$SCRIPTDIR/lib/apply_CHANGES.sh" - ;; - esac -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - get_USB_CTL -} - -main \ No newline at end of file diff --git a/lib/get_USB_CTL_GROUP.sh b/lib/get_USB_CTL_GROUP.sh deleted file mode 100755 index e2988cf..0000000 --- a/lib/get_USB_CTL_GROUP.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function get_USB_CTL_GROUP () { - clear - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - printf " -For this USB controller device to be passthrough-able, it must be the ONLY device in this group! -Passing through more than just the USB controller can in some cases cause system issues -if you do not know what you are doing. - -" - echo "#------------------------------------------#" - "$SCRIPTDIR/utils/ls-iommu" -i "$1" -F subclass_name:,name,device_id,optional_revision - echo "#------------------------------------------#" - - printf " -To use any of the devices shown for passthrough, all of them have to be passed through - -To return to the previous page just press ENTER. -" - read -r -p "Do you want to use the displayed devices for passthrough? [y/N]: " YESNO - - case "${YESNO}" in - [Yy]*) - # Get the PCI ids - local PCI_ID - PCI_ID=$("$SCRIPTDIR/utils/ls-iommu" -i "$1" --pciaddr | perl -pe "s/([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]{1})\n/\"\1\" /" | perl -pe "s/\s$//") - - # Replace the blank USB_CTL_ID with the PCI_ID for the usb controller the user wants to pass through - perl -pi -e "s/USB_CTL_ID=\(\)/USB_CTL_ID=\($PCI_ID\)/" "$SCRIPTDIR/$QUICKEMU/qemu-vfio_vars.conf" - exec "$SCRIPTDIR/lib/apply_CHANGES.sh" - ;; - *) - exec "$SCRIPTDIR/lib/get_USB_CTL.sh" - ;; - esac -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - get_USB_CTL_GROUP "$1" -} - -main "$1" diff --git a/lib/paths.sh b/lib/paths.sh deleted file mode 100755 index 16ca328..0000000 --- a/lib/paths.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2034 -MODPROBE="config/etc/modprobe.d" -INITRAMFS="config/etc/initramfs-tools" -ETCMODULES="config/etc/modules" -DEFAULT="config/etc/default" -QUICKEMU="config/quickemu" -DRACUT="config/etc/dracut.conf.d" -MKINITCPIO="config/etc/mkinitcpio.conf" - -READAPI="wget -O-" -DOWNLOAD="wget -0 \"$SCRIPTDIR/utils/ls-iommu.tar.gz\"" -# Get the tool to use for downloading -if [ -f "/usr/bin/curl" ]; -then - READAPI="curl" - DOWNLOAD="curl -JLo ls-iommu.tar.gz" -fi diff --git a/lib/set_CMDLINE.sh b/lib/set_CMDLINE.sh deleted file mode 100755 index 8a98203..0000000 --- a/lib/set_CMDLINE.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -# Function to configure systemd-boot using kernelstub -function set_KERNELSTUB () { - # Separator - printf " -############################################################ -" - - # Tell what we are going to do - echo "Adding vfio kernel arguments to systemd-boot using kernelstub" - - # Get the config paths - source "${SCRIPTDIR}/lib/paths.sh" - - # Check if systemd-boot already has vfio parameters from before - KERNELSTUB_TEST=$(sudo kernelstub -p 2>&1 | grep "Kernel Boot Options" | perl -pe "s/.+Kernel Boot Options:\..+(vfio_pci.ids=.+ ).+/\1/") - - # If there are already vfio_pci parameters in kernelstub - if [[ "$KERNELSTUB_TEST" =~ vfio_pci.ids ]] ; - then - # Remove the old parameters - sudo kernelstub -d "$KERNELSTUB_TEST" - sudo kernelstub -d "vfio_pci.disable_vga=1" - sudo kernelstub -d "vfio_pci.disable_vga=0" - fi - - # Apply new parameters - CMDLINE=$(cat "${SCRIPTDIR}/config/kernel_args") - sudo kernelstub -a "$CMDLINE" -} - -# Function to configure grub -function set_GRUB () { - # Separator - printf " -############################################################ -" - - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - local CMDLINE - CMDLINE=$(cat "$SCRIPTDIR/config/kernel_args") - - # HIGHLY EXPERIMENTAL! - local GRUB_CMDLINE - local GRUB_CMDLINE_LINUX - - # Check if there is a GRUB_CMDLINE_LINUX_DEFAULT line in grub config - if grep -q "GRUB_CMDLINE_LINUX_DEFAULT=" "$SCRIPTDIR/$DEFAULT/grub" ; - then - # Update the GRUB_CMDLINE_LINUX_DEFAULT line - GRUB_CMDLINE=$(grep -P "^GRUB_CMDLINE_LINUX_DEFAULT" "/etc/default/grub" | perl -pe "s/GRUB_CMDLINE_LINUX_DEFAULT=\"(.+)\"/\1/" | perl -pe "s/(amd|intel)_iommu=(on|1)|iommu=(pt|on)|vfio_pci.ids=.+|vfio_pci.disable_vga=\d{1}//g" | perl -pe "s/(^\s+|\s+$)//g") - GRUB_CMDLINE_LINUX=$(grep -P "^GRUB_CMDLINE_LINUX_DEFAULT" "/etc/default/grub") - perl -pi -e "s/${GRUB_CMDLINE_LINUX}/GRUB_CMDLINE_LINUX_DEFAULT=\"${GRUB_CMDLINE} ${CMDLINE}\"/" "${SCRIPTDIR}/$DEFAULT/grub" - else - # Update the GRUB_CMDLINE_LINUX line - GRUB_CMDLINE=$(grep -P "^GRUB_CMDLINE_LINUX" "/etc/default/grub" | perl -pe "s/GRUB_CMDLINE_LINUX=\"(.+)\"/\1/" | perl -pe "s/(amd|intel)_iommu=(on|1)|iommu=(pt|on)|vfio_pci.ids=.+|vfio_pci.disable_vga=\d{1}//g" | perl -pe "s/(^\s+|\s+$)//g") - GRUB_CMDLINE_LINUX=$(grep -P "^GRUB_CMDLINE_LINUX" "/etc/default/grub") - perl -pi -e "s/${GRUB_CMDLINE_LINUX}/GRUB_CMDLINE_LINUX=\"${GRUB_CMDLINE} ${CMDLINE}\"/" "${SCRIPTDIR}/$DEFAULT/grub" - fi - - - echo "The script will now replace your default grub file with a new one. -Then attempt to update grub and generate a new grub.cfg. -If generating the grub.cfg file fails, you can find a backup of your grub default file here: -$SCRIPTDIR/backup/etc/default/grub -" - read -r -p "Press ENTER to continue" - - sudo cp -v "$SCRIPTDIR/$DEFAULT/grub" "/etc/default/grub" - - # Generate grub.cfg - if [ -d "/boot/grub" ]; - then - sudo grub-mkconfig -o "/boot/grub/grub.cfg" - else - sudo grub-mkconfig -o "/boot/grub2/grub.cfg" - fi - - echo "" - read -r -p "Please verify there were no errors generating the grub.cfg file, then press ENTER" -} - -function show_FINISH () { - # Separator - printf " -############################################################ -" - - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - local CMDLINE - CMDLINE=$(cat "$SCRIPTDIR/config/kernel_args") - - echo "Configuration is now complete!" - - if [ "$1" == 0 ]; - then - printf "For VFIO to work properly you need to make sure these kernel parameters are in your bootloader entry: -#-----------------------------------------------# -%s -#-----------------------------------------------# - -" "$CMDLINE" - fi - - echo "Restart your system and run -\"$SCRIPTDIR/vfio-verify\" -to check if your GPU is properly set up. - -If the graphic card is bound to vfio-pci then you can -proceed to add it to your virtual machines. - -A backup the files we replaced on your system can be found inside -$SCRIPTDIR/backup/ -In order to restore these files just copy them back to your system and -rebuild your initramfs image. - -You can remove the the vfio_pci kernel arguments from the linux line in your bootloader -to disable/unbind the graphic card from the vfio driver on boot. - -The files inside \"$SCRIPTDIR/$QUICKEMU\" are currently unused files, however they provide -the required information that the QuickEMU project can hook into and use to add support for VFIO enabled VMs. - -The PCI Devices with these IDs are what you should add to your VMs using Virt Manager: -NOTE: Some AMD GPUs will require the vendor-reset kernel module from https://github.com/gnif/vendor-reset to be installed!" - - source "${SCRIPTDIR}/config/quickemu/qemu-vfio_vars.conf" - - for dev in "${GPU_PCI_ID[@]}" - do - echo "* $dev" - done - for dev in "${USB_CTL_ID[@]}" - do - echo "* $dev" - done - -echo " -To add the graphic card to your VM using qemu directly, use the following arguments:" - for dev in "${GPU_PCI_ID[@]}" - do - - echo -n "-device vfio-pci,host=$dev " - done - printf "\n" - - echo " -For performance tuning and advanced configuration look at: -https://github.com/HikariKnight/vfio-setup-docs/wiki" -} - -function set_CMDLINE () { - # Make a variable to tell if - local BOOTLOADER_AUTOCONFIG - BOOTLOADER_AUTOCONFIG=0 - - # If kernelstub is detected (program to manage systemd-boot) - if which kernelstub > /dev/null 2>&1 ; - then - # Configure kernelstub - set_KERNELSTUB - BOOTLOADER_AUTOCONFIG=1 - fi - - # If grub exists - if which grub-mkconfig > /dev/null 2>&1 ; - then - # Configure grub - set_GRUB - BOOTLOADER_AUTOCONFIG=1 - fi - - show_FINISH $BOOTLOADER_AUTOCONFIG -} - - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_CMDLINE -} - -main diff --git a/lib/set_DRACUT.sh b/lib/set_DRACUT.sh deleted file mode 100755 index 4dc0270..0000000 --- a/lib/set_DRACUT.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function set_DRACUT () { - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - # Write the dracut config - echo "add_drivers+=\" vfio_pci vfio vfio_iommu_type1 vfio_virqfd \"" > "$SCRIPTDIR/$DRACUT/10-vfio.conf" - - # Get the kernel_args file content - CMDLINE=$(cat "$SCRIPTDIR/config/kernel_args") - - # Update kernel_args to load the vfio_pci module early in dracut (as dracut uses kernel arguments for early loading) - echo "$CMDLINE rd.driver.pre=vfio_pci" > "$SCRIPTDIR/config/kernel_args" - - # Bind GPU to VFIO - "$SCRIPTDIR/lib/set_VFIO.sh" "$1" - - # Configure modprobe - exec "$SCRIPTDIR/lib/set_MODPROBE.sh" "$1" -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_DRACUT "$1" -} - -main "$1" \ No newline at end of file diff --git a/lib/set_INITRAMFSTOOLS.sh b/lib/set_INITRAMFSTOOLS.sh deleted file mode 100755 index 2eea550..0000000 --- a/lib/set_INITRAMFSTOOLS.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function insert_INITRAMFSTOOLS() { - # Get the header and enabled modules separately from the /etc/modules file - local MODULES_HEADER - local MODULES_ENABLED - local VENDOR_RESET - MODULES_HEADER=$(head -n "$1" "$2" | grep -P "^#" | grep -v "# Added by quickpassthrough") - MODULES_ENABLED=$(grep -vP "^#" "$2" | grep -v "vendor-reset" | perl -pe "s/^\n//") - VENDOR_RESET=0 - - # If vendor-reset is present - if grep -q "vendor-reset" "$2" ; - then - VENDOR_RESET=1 - fi - - # Write header - echo "$MODULES_HEADER" > "$2" - - # If vendor-reset existed from before - if [ $VENDOR_RESET == 1 ]; - then - # Write vendor-reset as the first module! - echo "vendor-reset" >> "$2" - fi - - # Append vfio - printf " -# Added by quickpassthrough # -vfio -vfio_iommu_type1 -vfio_pci -vfio_virqfd -############################# -" >> "$2" - - # Write the previously enabled modules under vfio in the load order - echo "$MODULES_ENABLED" >> "$2" -} - -function set_INITRAMFSTOOLS () { - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - # Insert modules in the correct locations as early as possible without - # conflicting with vendor-reset module if it is enabled - insert_INITRAMFSTOOLS 4 "$SCRIPTDIR/$ETCMODULES" - insert_INITRAMFSTOOLS 11 "$SCRIPTDIR/$INITRAMFS/modules" - - # Bind GPU to VFIO - "$SCRIPTDIR/lib/set_VFIO.sh" "$1" - - # Configure modprobe - exec "$SCRIPTDIR/lib/set_MODPROBE.sh" "$1" -} - - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_INITRAMFSTOOLS "$1" -} - -main "$1" diff --git a/lib/set_MKINITCPIO.sh b/lib/set_MKINITCPIO.sh deleted file mode 100755 index 34ea8b4..0000000 --- a/lib/set_MKINITCPIO.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function set_MKINITCPIO () { - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - # Grab the current modules but exclude vfio and vendor-reset - CURRENTMODULES=$(grep -P "^MODULES" "$SCRIPTDIR/$MKINITCPIO" | perl -pe "s/MODULES=\((.+)\)/\1/") - MODULES="$(grep -P "^MODULES" "$SCRIPTDIR/$MKINITCPIO" | perl -pe "s/MODULES=\((.+)\)/\1/" | perl -pe "s/\s?(vfio_iommu_type1|vfio_pci|vfio_virqfd|vfio|vendor-reset)\s?//g")" - - # Check if vendor-reset is present - if [[ $CURRENTMODULES =~ "vendor-reset" ]]; - then - # Inject vfio modules with vendor-reset - perl -pi -e "s/MODULES=\(${CURRENTMODULES}\)/MODULES=\(vendor-reset vfio vfio_iommu_type1 vfio_pci vfio_virqfd ${MODULES}\)/" "$SCRIPTDIR/$MKINITCPIO" - else - # Inject vfio modules - perl -pi -e "s/MODULES=\(${CURRENTMODULES}\)/MODULES=\(vfio vfio_iommu_type1 vfio_pci vfio_virqfd ${MODULES}\)/" "$SCRIPTDIR/$MKINITCPIO" - fi - - # Bind GPU to VFIO - "$SCRIPTDIR/lib/set_VFIO.sh" "$1" - - # Configure modprobe - exec "$SCRIPTDIR/lib/set_MODPROBE.sh" "$1" -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_MKINITCPIO "$1" -} - -main "$1" \ No newline at end of file diff --git a/lib/set_MODPROBE.sh b/lib/set_MODPROBE.sh deleted file mode 100755 index 6858bed..0000000 --- a/lib/set_MODPROBE.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function set_MODPROBE () { - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - # Assign the GPU device ids to a variable - GPU_DEVID="$1" - - # If VGA is disabled - if grep -q "vfio_pci.disable_vga=1" "$SCRIPTDIR/config/kernel_args" ; - then - # Modify our GPU_DEVID line to containe disable_vga=1 - GPU_DEVID="${GPU_DEVID} disable_vga=1" - fi - - # Write the vfio modprobe config - printf "## This is an autogenerated file that stubs your graphic card for use with vfio -## This file should be placed inside /etc/modprobe.d/ -# Uncomment the line below to \"hardcode\" your graphic card to be bound to the vfio-pci driver. -# In most cases this should not be neccessary, it will also prevent you from turning off vfio in the bootloader. -#options vfio_pci ids=%s - -# Make sure vfio_pci is loaded before these modules: nvidia, nouveau, amdgpu and radeon -softdep nvidia pre: vfio vfio_pci -softdep nouveau pre: vfio vfio_pci -softdep amdgpu pre: vfio vfio_pci -softdep radeon pre: vfio vfio_pci -" "${GPU_DEVID}" > "$SCRIPTDIR/$MODPROBE/vfio.conf" - - exec "$SCRIPTDIR/lib/get_USB_CTL.sh" -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_MODPROBE "$1" -} - -main "$1" \ No newline at end of file diff --git a/lib/set_VFIO.sh b/lib/set_VFIO.sh deleted file mode 100755 index 7960f21..0000000 --- a/lib/set_VFIO.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -function set_VFIO () { - # Get the config paths - source "$SCRIPTDIR/lib/paths.sh" - - # Assign the GPU device ids to a variable - GPU_DEVID="$1" - - # Get the kernel_args file content - CMDLINE=$(cat "$SCRIPTDIR/config/kernel_args") - - # Ask if we shall disable video output on this card - echo " -Disabling video output in Linux for the card you want to use in a VM -will make it easier to successfully do the passthrough without issues." - read -r -p "Do you want to force disable video output in linux on this card? [Y/n]: " DISABLE_VGA - case "${DISABLE_VGA}" in - [Yy]*) - # Update kernel_args file - echo "${CMDLINE} vfio_pci.ids=${GPU_DEVID} vfio_pci.disable_vga=1" > "$SCRIPTDIR/config/kernel_args" - - # Update GPU_DEVID - GPU_DEVID="$GPU_DEVID disable_vga=1" - ;; - [Nn]*) - # Update kernel_args file - echo "${CMDLINE} vfio_pci.ids=${GPU_DEVID}" > "$SCRIPTDIR/config/kernel_args" - ;; - *) - # Update kernel_args file - echo "${CMDLINE} vfio_pci.ids=${GPU_DEVID} vfio_pci.disable_vga=1" > "$SCRIPTDIR/config/kernel_args" - - # Update GPU_DEVID - GPU_DEVID="$GPU_DEVID disable_vga=1" - ;; - esac -} - -function main () { - SCRIPTDIR=$(dirname "$(realpath "$0")" | perl -pe "s/\/\.\.\/lib//" | perl -pe "s/\/lib$//") - - set_VFIO "$1" -} - -main "$1" \ No newline at end of file diff --git a/pkg/command/command.go b/pkg/command/command.go new file mode 100644 index 0000000..9c4a0e4 --- /dev/null +++ b/pkg/command/command.go @@ -0,0 +1,98 @@ +package command + +import ( + "bytes" + "encoding/base64" + "io" + "os" + "os/exec" + "time" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" +) + +func Run(binary string, args ...string) ([]string, error) { + var stdout, stderr bytes.Buffer + + // Configure the ls-iommu c--ommand + cmd := exec.Command(binary, args...) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + // Execute the command + err := cmd.Run() + + // Read the output + output, _ := io.ReadAll(&stdout) + + // Get the output + outputs := []string{} + outputs = append(outputs, string(output)) + + // Return our list of items + return outputs, err +} + +// This function is just like command.Run() but also returns STDERR +func RunErr(binary string, args ...string) ([]string, []string, error) { + var stdout, stderr bytes.Buffer + + // Configure the ls-iommu c--ommand + cmd := exec.Command(binary, args...) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + // Execute the command + err := cmd.Run() + + // Read the output + output, _ := io.ReadAll(&stdout) + outerr, _ := io.ReadAll(&stderr) + + // Get the output + var outputs, outerrs []string + outputs = append(outputs, string(output)) + outerrs = append(outerrs, string(outerr)) + + // Return our list of items + return outputs, outerrs, err +} + +// This functions runs the command "sudo -Sk -- echo", this forces sudo +// to re-authenticate and lets us enter the password to STDIN +// giving us the ability to run sudo commands +func Elevate(password string) { + // Do a simple sudo command to just authenticate with sudo + cmd := exec.Command("sudo", "-S", "--", "echo") + + // Wait for 500ms, if the password is correct, sudo will return immediately + cmd.WaitDelay = 1000 * time.Millisecond + + // Open STDIN + stdin, err := cmd.StdinPipe() + errorcheck.ErrorCheck(err, "\nFailed to get sudo STDIN") + + // Start the authentication + cmd.Start() + + // Get the passed password + pw, _ := base64.StdEncoding.DecodeString(password) + _, err = stdin.Write([]byte(string(pw) + "\n")) + errorcheck.ErrorCheck(err, "\nFailed at typing to STDIN") + // Clear the password + pw = nil + password = "" + + stdin.Close() + + // Wait for the sudo prompt (If the correct password was given, it will not stay behind) + err = cmd.Wait() + errorcheck.ErrorCheck(err, "\nError, password given was wrong") +} + +// Function to just clear the terminal +func Clear() { + c := exec.Command("clear") + c.Stdout = os.Stdout + c.Run() +} diff --git a/pkg/fileio/fileio.go b/pkg/fileio/fileio.go new file mode 100644 index 0000000..38c5a7a --- /dev/null +++ b/pkg/fileio/fileio.go @@ -0,0 +1,99 @@ +package fileio + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" +) + +/* + * This just implements repetetive tasks I have to do with files + */ + +// Creates a file and appends the content to the file (ending newline must be supplied with content string) +func AppendContent(content string, fileName string) { + // Open the file + f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) + errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening \"%s\" for writing", fileName)) + defer f.Close() + + // Write the content + _, err = f.WriteString(content) + errorcheck.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName)) +} + +// Reads the file and returns a stringlist with each line +func ReadLines(fileName string) []string { + content, err := os.Open(fileName) + errorcheck.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName)) + defer content.Close() + + // Make a list of lines + var lines []string + + // Read the file line by line + scanner := bufio.NewScanner(content) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + // Return all the lines + return lines + +} + +// Reads a file and returns all the content as a string +func ReadFile(fileName string) string { + // Read the whole file + content, err := os.ReadFile(fileName) + errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName)) + + // Return all the lines as one string + return string(content) + +} + +// Checks if a file exists and returns a bool +func FileExist(fileName string) bool { + var exist bool + + // Check if the file exists + if _, err := os.Stat(fileName); !errors.Is(err, os.ErrNotExist) { + // Set the value to true + exist = true + } else { + // Set the value to false + exist = false + } + + // Return if the file exists + return exist +} + +// Copies a FILE from source to dest +func FileCopy(sourceFile, destFile string) { + // Get the file info + filestat, err := os.Stat(sourceFile) + errorcheck.ErrorCheck(err, "Error getting fileinfo of: %s", sourceFile) + + // If the file is a regular file + if filestat.Mode().IsRegular() { + // Open the source file for reading + source, err := os.Open(sourceFile) + errorcheck.ErrorCheck(err, "Error opening %s for copying", sourceFile) + defer source.Close() + + // Create the destination file + dest, err := os.Create(destFile) + errorcheck.ErrorCheck(err, "Error creating %s", destFile) + defer dest.Close() + + // Copy the contents of source to dest using io + _, err = io.Copy(dest, source) + errorcheck.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile) + } +} diff --git a/pkg/menu/genmenu.go b/pkg/menu/genmenu.go new file mode 100644 index 0000000..f1e917a --- /dev/null +++ b/pkg/menu/genmenu.go @@ -0,0 +1,40 @@ +package menu + +import ( + "regexp" + + "github.com/gookit/color" + "github.com/nexidian/gocliselect" +) + +func GenIOMMUMenu(msg string, choices []string, none_option ...int) string { + // Make a regex to get the iommu group + iommu_group_regex := regexp.MustCompile(`(\d{1,3})`) + + // Make the menu + menu := gocliselect.NewMenu(msg) + + // For each choice passed + for _, choice := range choices { + // Get the iommu group + iommuGroup := iommu_group_regex.FindString(choice) + + // Add the choice with shortened vendor name and the iommu group as the return value + menu.AddItem(choice, iommuGroup) + } + + // If none_option is higher than 0 + if len(none_option) > 0 { + // Add a skip option + menu.AddItem(color.Bold.Sprint("Skip/None"), "skip") + } + + // Add a go back option + menu.AddItem(color.Bold.Sprint("Go Back"), "back") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} diff --git a/pkg/menu/manual.go b/pkg/menu/manual.go new file mode 100644 index 0000000..5e0b06c --- /dev/null +++ b/pkg/menu/manual.go @@ -0,0 +1,27 @@ +package menu + +import ( + "fmt" + "strings" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/gookit/color" +) + +func ManualInput(msg string, format string) []string { + // Print the title + color.Bold.Println(msg) + + // Tell user the format to use + color.Bold.Printf("The format is %s\n", format) + + // Get the user input + var input string + _, err := fmt.Scan(&input) + errorcheck.ErrorCheck(err) + + input_list := strings.Split(input, ",") + + // Return the input + return input_list +} diff --git a/pkg/menu/next.go b/pkg/menu/next.go new file mode 100644 index 0000000..c38d30e --- /dev/null +++ b/pkg/menu/next.go @@ -0,0 +1,20 @@ +package menu + +import ( + "github.com/gookit/color" + "github.com/nexidian/gocliselect" +) + +// Make a Next menu +func Next(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem(color.Bold.Sprint("Next"), "next") + menu.AddItem(color.Bold.Sprint("Go Back"), "back") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} diff --git a/pkg/menu/ok.go b/pkg/menu/ok.go new file mode 100644 index 0000000..85930cb --- /dev/null +++ b/pkg/menu/ok.go @@ -0,0 +1,33 @@ +package menu + +import ( + "github.com/gookit/color" + "github.com/nexidian/gocliselect" +) + +// Make an OK menu +func Ok(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem("OK", "next") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} + +// Make an OK & Go Back menu +func OkBack(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem(color.Bold.Sprint("OK"), "next") + menu.AddItem(color.Bold.Sprint("Go Back"), "back") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} diff --git a/pkg/menu/yesno.go b/pkg/menu/yesno.go new file mode 100644 index 0000000..03239ce --- /dev/null +++ b/pkg/menu/yesno.go @@ -0,0 +1,63 @@ +package menu + +import ( + "github.com/gookit/color" + "github.com/nexidian/gocliselect" +) + +// Make a YesNo menu +func YesNo(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem("Yes", "y") + menu.AddItem("No", "n") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} + +func YesNoBack(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem("Yes", "y") + menu.AddItem("No", "n") + menu.AddItem(color.Bold.Sprint("Go Back"), "back") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} + +func YesNoEXT(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem("Yes", "y") + menu.AddItem("No", "n") + menu.AddItem("ADVANCED: View with extended related search by vendor ID, devices listed might not be related", "ext") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} + +// Make a YesNo menu +func YesNoManual(msg string) string { + // Make the menu + menu := gocliselect.NewMenu(msg) + menu.AddItem("Yes", "y") + menu.AddItem("No", "n") + menu.AddItem("Manual Entry", "manual") + + // Display the menu + choice := menu.Display() + + // Return the value selected + return choice +} diff --git a/pkg/uname/uname.go b/pkg/uname/uname.go new file mode 100644 index 0000000..4a87c62 --- /dev/null +++ b/pkg/uname/uname.go @@ -0,0 +1,56 @@ +package uname + +import "syscall" + +type Uname struct { + Sysname string + Nodename string + Hostname string + Release string + Kernel string + Version string + Machine string + Arch string + Domainname string +} + +// A utility to convert int8 values to proper strings. +func int8ToStr(arr []int8) string { + b := make([]byte, 0, len(arr)) + for _, v := range arr { + if v == 0x00 { + break + } + b = append(b, byte(v)) + } + return string(b) +} + +func New() *Uname { + var system syscall.Utsname + uname := &Uname{} + if err := syscall.Uname(&system); err == nil { + // extract members: + // type Utsname struct { + // Sysname [65]int8 + // Nodename [65]int8 + // Release [65]int8 + // Version [65]int8 + // Machine [65]int8 + // Domainname [65]int8 + // } + + // Add to the uname struct for humans + uname.Sysname = int8ToStr(system.Sysname[:]) + uname.Nodename = int8ToStr(system.Nodename[:]) + uname.Hostname = uname.Nodename + uname.Release = int8ToStr(system.Release[:]) + uname.Kernel = uname.Release + uname.Version = int8ToStr(system.Version[:]) + uname.Machine = int8ToStr(system.Machine[:]) + uname.Arch = uname.Machine + uname.Domainname = int8ToStr(system.Domainname[:]) + } + + return uname +} diff --git a/pkg/untar/untar.go b/pkg/untar/untar.go new file mode 100644 index 0000000..96ab454 --- /dev/null +++ b/pkg/untar/untar.go @@ -0,0 +1,83 @@ +package untar + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" +) + +// Slightly modified from source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07 + +// Untar takes a destination path and a path to a file; a tar reader loops over the tarfile +// creating the file structure at 'dst' along the way, and writing any files +func Untar(dst string, fileName string) error { + r, err := os.Open(fileName) + errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName)) + defer r.Close() + + gzr, err := gzip.NewReader(r) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + + switch { + + // if no more files are found return + case err == io.EOF: + return nil + + // return any other error + case err != nil: + return err + + // if the header is nil, just skip it (not sure how this happens) + case header == nil: + continue + } + + // the target location where the dir/file should be created + target := filepath.Join(dst, header.Name) + + // the following switch could also be done using fi.Mode(), not sure if there + // a benefit of using one vs. the other. + // fi := header.FileInfo() + + // check the file type + switch header.Typeflag { + + // if its a dir and it doesn't exist create it + case tar.TypeDir: + if _, err := os.Stat(target); err != nil { + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + } + + // if it's a file create it + case tar.TypeReg: + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + // copy over contents + if _, err := io.Copy(f, tr); err != nil { + return err + } + + // manually close here after each file operation; defering would cause each file close to wait until all operations have completed. + f.Close() + } + } +} diff --git a/quickpassthrough_preview.gif b/quickpassthrough_preview.gif new file mode 100644 index 0000000..a40bd75 Binary files /dev/null and b/quickpassthrough_preview.gif differ diff --git a/vfio-setup b/vfio-setup deleted file mode 100755 index 338c60d..0000000 --- a/vfio-setup +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091 - -# Get the scripts directory -SCRIPTDIR=$(dirname "$(realpath "$0")") -cd "$SCRIPTDIR" || exit - -# Get the config paths -source "$SCRIPTDIR/lib/paths.sh" - -# Get the CPU Vendor -CPU_VENDOR=$(grep "vendor_id" /proc/cpuinfo | head -1 | cut -f 2 | cut -d " " -f 2) -CMDLINE="iommu=pt" - -# Adjust our kernel_args based on cpu vendor -if [ "$CPU_VENDOR" == "GenuineIntel" ]; -then - CMDLINE="$CMDLINE intel_iommu=on" -elif [ "$CPU_VENDOR" == "AuthenticAMD" ]; -then - CMDLINE="$CMDLINE amd_iommu=on" -fi - -# Clear the screen -clear - -# Show the user a warning before we start -printf "Welcome to QuickPassthrough! -Written for systems using initramfs. - -The setup done by this script is quite complex and is prone to human error or hardware incompatibilities. -It is HIGHLY RECOMMENDED to make a backup/snapshot of your system using something like timeshift or snapper before starting. - -Once everything is configured, your 2nd graphic card will hopefully be dedicated for use inside a virtual machine. - -Even though that this script is intended to make this setup easier, it is recommended that you -read through a guide and learn what is actually being done so that you can get familiar the process. -A full documentation for debian/ubuntu systems can be found here: https://github.com/hikariknight/vfio-setup-docs/wiki - -Press ENTER to continue once you have made a backup of your system. -" -read -r - -# Separator -printf " -############################################################ -" - -echo "This script assumes a few things: -* You have already enabled IOMMU, VT-d and/or AMD-v inside your UEFI/BIOS advanced settings. -* You have already added \"$CMDLINE\" to your - kernel boot arguments and booted your system with these kernel arguments active. -* You are comfortable with navigating and changing settings in your UEFI/BIOS. -* You know how edit your bootloader configuration and kernel arguments. -* Your Linux distribution is an EFI installation (important to get VFIO working). - -NOTE: If your computer no longer fully shut down after enabling IOMMU, then there is possibly a bug - with your motherboard and a piece of hardware in your system, it only prevents you from using - the system in a headless mode with working shutdown and is otherwise just an annoying - quirk with IOMMU on some boards. - -This is a list of prerequisites you will be needing before starting with VFIO: -* 2 very different GPUs (iGPU/APU included), the easiest combination is to have 2 from different vendors (amd/intel/nvidia) - if both cards share the same device id (ex: both are identified as 1022:145c), then passthrough will - not be possible unless you swap out one of the cards. -* A \"ghost display\" dummy plug for your second graphic card (or having it hooked to a separate input on your monitor). -* If you are planning to use the integrated GPU on your CPU, make sure your monitor is connected to it and working/enabled before continuing. -* Preferably a motherboard verified to work with IOMMU and with good IOMMU groups. - https://reddit.com/r/vfio is a good resource for this info. - (If you are unsure, you will find out while using this script) - -* It is also highly recommended to have access to your VM through VNC (RDP will not work) as once you pass through - a graphic card, it will most likely not possible to interact with it through spice and the normal qemu display window! - -Press ENTER to start creating your config from scratch. -NOTE: continuing will delete the contents of \"$SCRIPTDIR/config\" -" -read -r - -# Separator -printf " -############################################################ -" - -if [ ! -d "$SCRIPTDIR/utils" ]; -then - mkdir "$SCRIPTDIR/utils" -fi - -if [ ! -f "$SCRIPTDIR/utils/ls-iommu.tar.gz" ]; -then - printf "Checking for newest version of ls-iommu\n" - LS_IOMMU_VERSION=$($READAPI https://api.github.com/repos/hikariknight/ls-iommu/releases/latest | grep "tag_name" | perl -pe "s/.+\"tag_name\"\:\s+\"(\d+\.\d+\.\d+)\",/\1/") - printf "Downloading required tool ls-iommu from https://github.com/hikariknight/ls-iommu\n" - cd "$SCRIPTDIR/utils" || exit - $DOWNLOAD https://github.com/HikariKnight/ls-iommu/releases/download/${LS_IOMMU_VERSION}/ls-iommu_${LS_IOMMU_VERSION}_Linux_x86_64.tar.gz - tar -zxf "$SCRIPTDIR/utils/ls-iommu.tar.gz" --directory "$SCRIPTDIR/utils/" -else - if [ ! -f "$SCRIPTDIR/utils/ls-iommu" ]; - then - tar -zxf "$SCRIPTDIR/utils/ls-iommu.tar.gz" --directory "$SCRIPTDIR/utils/" - fi -fi - -if [ -d "$SCRIPTDIR/config" ]; -then - rm -r "$SCRIPTDIR/config" -fi - -# Make the directories -mkdir -p "$SCRIPTDIR/$MODPROBE" -mkdir -p "$SCRIPTDIR/$QUICKEMU" - -# Write the cmdline file -echo "$CMDLINE" > "$SCRIPTDIR/config/kernel_args" - -# Copy system configs that exists into our config folder so we can safely edit them -if [ -f "/etc/modules" ]; -then - # This copies /etc/modules without the vfio module lines - grep -v "vfio" "/etc/modules" > "$SCRIPTDIR/$ETCMODULES" -fi - -if [ -f "/etc/default/grub" ]; -then - # Create the default folder - mkdir -p "$SCRIPTDIR/$DEFAULT" - - # Copy grub config - cp "/etc/default/grub" "$SCRIPTDIR/$DEFAULT/grub" -fi - -if [ -f "/etc/initramfs-tools/modules" ]; -then - # Create the initramfs folder - mkdir -p "$SCRIPTDIR/$INITRAMFS" - - # This copies /etc/initramfs-tools/modules without the vfio modules - grep -v "vfio" "/etc/initramfs-tools/modules" > "$SCRIPTDIR/$INITRAMFS/modules" -fi - -if [ -f "/etc/mkinitcpio.conf" ]; -then - # Copy mkinitcpio.conf to our config folder - cp "/etc/mkinitcpio.conf" "$SCRIPTDIR/$MKINITCPIO" -fi - -if [ -f "/etc/dracut.conf" ]; -then - # Create the dracut folder - mkdir -p "$SCRIPTDIR/$DRACUT" - # Create a dracut.conf.d file - touch "$SCRIPTDIR/$DRACUT/10-vfio.conf" -fi - -# Run ls-iommu so we can verify that IOMMU properly working -LS_IOMMU=$($SCRIPTDIR/utils/ls-iommu) - -# Detect if IOMMU is disabled (output will be empty) -if [[ "$LS_IOMMU" == "" ]]; -then - # Tell user to enable IOMMU then try again - echo "IOMMU IS NOT ENABLED! -Please enable IOMMU, VT-d or AMD-v inside your UEFI/BIOS and add \"$CMDLINE\" -to your kernel boot arguments and reboot your system, then re-run this script! -" - exit 1 - -else - # Show the output of ls-iommu for manual verification - echo "$LS_IOMMU" -fi - -echo "" - -# Have user visually verify that IOMMU is working -read -r -p "Is there more than 1 group in the output above? [y/N]: " YESNO -case "${YESNO}" in - [Yy]*) - echo "" - ;; - [Nn]*) - echo "Please enable IOMMU, VT-d or AMD-v inside your UEFI/BIOS and add \"$CMDLINE\" -to your kernel boot arguments and reboot your system, then re-run this script! -" - exit 1 - ;; - *) - echo "Please enable IOMMU, VT-d or AMD-v inside your UEFI/BIOS and add \"$CMDLINE\" -to your kernel boot arguments and reboot your system, then re-run this script! -" - exit 1 - ;; -esac - -exec "$SCRIPTDIR/lib/get_GPU.sh" diff --git a/vfio-verify b/vfio-verify deleted file mode 100755 index ed2ed5e..0000000 --- a/vfio-verify +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2002,SC2164 - -# Get the scripts directory -SCRIPTDIR=$(dirname "$(which "$0")") -cd "$SCRIPTDIR" - -# If there is a config generated, then $SCRIPTDIR/config/kernel_args -# should exist, which contains all the info we need -if [ -f "$SCRIPTDIR/config/kernel_args" ]; -then - echo "#------------------------------------------#" - # List info about the vfio gpu and what kernel driver is being used - "$SCRIPTDIR/utils/ls-iommu" -g -r -k - echo "#------------------------------------------#" - - printf " -If the \"Kernel driver in use\" for the passed through devices are \"vfio-pci\", then VFIO has been successfully enabled! -GPU Devices using \"pcieport\" can be ignored. - -NOTE: If your system freezes when starting a VM that uses your VFIO enabled card.. -consider adding the below line to your bootloaders kernel arguments: -vfio_pci.disable_vga=1 - -That will disable video output from the card while it is connected to the Linux host. -" - -else - # Tell user to run the setup first if the kernel_args file is not found - echo "Please run \"$SCRIPTDIR/vfio-setup\" first!" - -fi \ No newline at end of file