Compare commits

..

18 commits
2.0.4 ... main

Author SHA1 Message Date
jedrw
c1f11ce1c3 fix: avoid potential index out of range error if ls-iommu cannot find a vbios path
Signed-off-by: HikariKnight <2557889+HikariKnight@users.noreply.github.com>
2025-01-06 14:43:55 +01:00
HikariKnight
0efcf4ee19 fix(dracut): force load vfio modules earlier
this should make them load before any gpu drivers
2025-01-06 14:15:45 +01:00
HikariKnight
276cfd953a fix: correct vfio_pci to vfio-pci in rd.driver.pre for dracut 2024-12-29 05:34:03 +01:00
HikariKnight
4f51fd5c5f chore: remove outdated info from readme 2024-09-08 02:08:09 +02:00
HikariKnight
05afa8c543 chore: cleanup the readme 2024-08-10 09:11:03 +02:00
HikariKnight
d1e44d94d5 chore: add quickpassthrough_debug.log to gitignore 2024-08-10 09:02:49 +02:00
HikariKnight
3f934351fc fix: correctly detect kernel 6.10 and newer as a newer kernel 2024-08-10 08:57:54 +02:00
HikariKnight
380c875213 fix: CopyToSystem now correctly uses ExecAndLogSudo 2024-07-27 17:49:46 +02:00
kayos
6c48a35180
Feat: conditional permissions behavior (#28)
* Fix: don't need sudo if we're root + other aesthetics

* Heavy refactoring, see PR #28

* Fix: avoid silent fatalities

demo: https://tcp.ac/i/JMSUc.gif

* Fix: Inverse check on `IsRoot`

* D.R.Y: check for permissions error in `common.ErrorCheck`

Reduce cognitive complexity.

* Fix: Issue with copying

* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646535918

* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105

* Revert "Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105"

This reverts commit ce15213009.

* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646730751
2024-07-27 17:38:39 +02:00
HikariKnight
4d0086df41 feat: Generate dummy rom for amd RX7000 series cards
By recommendation from gnif on discord.

Co-authored-by: Geoffrey McRae <geoff@hostfission.com>
2024-06-15 02:53:38 +02:00
HikariKnight
1002f4d64c chore: add missing on comment for command.Run 2024-06-15 02:17:53 +02:00
HikariKnight
0273aabecc chore: change quickemu folder to qemu folder 2024-06-15 01:51:57 +02:00
HikariKnight
47567c11fb chore: update funding 2024-04-24 21:44:18 +02:00
HikariKnight
3bda7cadfc chore: update name of the github job 2024-04-02 11:37:07 +02:00
HikariKnight
a9b889e5ac fix: update download url for ls-iommu checksums.txt
after implementing github actions the checksums file was made versioned
2024-04-02 11:30:41 +02:00
HikariKnight
82e21005d5 chore: update readme with link to lantian's documentation for laptop gpus 2024-03-31 00:41:12 +01:00
HikariKnight
16fe298cb9 chore: update readme with new information 2024-03-30 17:18:16 +01:00
Matt Spurrier
8bd34b293e
remove pull-requests from release triggers, only trigger on tags. (#25) 2024-03-30 15:38:09 +01:00
26 changed files with 681 additions and 317 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: HikariKnight # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: HikariKnight

View file

@ -1,7 +1,6 @@
name: goreleaser
name: Build & Release
on:
pull_request:
push:
# run only against tags
tags:
@ -37,4 +36,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}

3
.gitignore vendored
View file

@ -6,4 +6,5 @@ bin/
dist/
main
quickpassthrough
debug.log
debug.log
quickpassthrough_debug.log

View file

@ -3,45 +3,12 @@
A project to simplify setting up GPU passthrough on your Linux host for libvirt/virt-manager
![Quickpassthrough Preview GIF](https://raw.githubusercontent.com/HikariKnight/quickpassthrough/main/quickpassthrough_preview.gif)
You can use it by simply downloading the latest [release](https://github.com/HikariKnight/quickpassthrough/releases/) and run it inside a terminal/shell or by downloading and compiling it yourself with the commands below.
You can use it by simply downloading the latest [release](https://github.com/HikariKnight/quickpassthrough/releases/) and run it inside a terminal 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
go mod download
CGO_ENABLED=0 go build -o quickpassthrough cmd/main.go
```
This project is aimed at **systems with 2 GPUs** and **headless servers**, where the only GPU is not needed.
## 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
```
**Note:** Quickpassthrough is not designed to be installed by a package manager! As you would usually have to only run it once, unless you change the GPU.
## 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. <br>
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
@ -56,18 +23,55 @@ NOTE: This project is aimed at desktops and headless servers.
* 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
## Features now handled by [ls-iommu](https://github.com/HikariKnight/ls-iommu)
## Features 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
## Build with current dependencies
```bash
git clone https://github.com/HikariKnight/quickpassthrough.git
cd quickpassthrough
go mod download
CGO_ENABLED=0 go build -ldflags="-X github.com/HikariKnight/quickpassthrough/internal/version.Version=$(git rev-parse --short HEAD)" -o quickpassthrough cmd/main.go
```
## 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 -ldflags="-X github.com/HikariKnight/quickpassthrough/internal/version.Version=$(git rev-parse --short HEAD)" -o quickpassthrough cmd/main.go
```
## Does this work on atomic or immutable systems?
Currently no, however [Bazzite](https://bazzite.gg), [Bluefin](https://projectbluefin.io) and [Aurora](https://getaurora.dev) has an `ujust` command that does a very similar job.
In Bazzite you run `ujust setup-virtualization` and follow the prompts to `Enable Virtualization` and `Enable VFIO drivers`.<br>
In Bluefin and Aurora you run `ujust setup-vfio` and follow the prompts.
## 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.
Compare that folder with the `config/` folder to see which files you need to delete in addition to copying the files from `backup/` to your system before rebuilding your initramfs and updating your bootloader config.
## 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. <br>
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)
## Contributing
<img src="https://user-images.githubusercontent.com/2557889/156038229-4e70352f-9182-4474-8e32-d14d3ad67566.png" width="250px">
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. <br>
So the project moved over to golang, this lets us utilize TUI toolkits like to build a proper menu system for the project. <br>
So the project moved over to golang, this lets us utilize TUI toolkits to build a proper menu system for the project. <br>
If you know golang, passthrough or qemu, you are welcome to help! Just make a pull request!<br>
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. 😄

50
internal/common/errors.go Normal file
View file

@ -0,0 +1,50 @@
package common
import (
"errors"
"os"
"time"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/gookit/color"
)
const PermissionNotice = `
<yellowB>Permissions error occured during file operations.</>
<blue_b>Hint</>:
If you initially ran QuickPassthrough as root or using sudo,
but are now running it as a normal user, this is expected behavior.
<us>Try running QuickPassthrough as root or using sudo if so.</>
If this does not work, double check your filesystem's permissions,
and be sure to check the debug log for more information.
`
// ErrorCheck serves as a wrapper for HikariKnight/ls-iommu/pkg/common.ErrorCheck that allows for visibile error messages
func ErrorCheck(err error, msg ...string) {
_, _ = os.Stdout.WriteString("\033[H\033[2J") // clear the screen
if err == nil {
return
}
if errors.Is(err, os.ErrPermission) {
color.Printf(PermissionNotice)
}
oneMsg := ""
if len(msg) < 1 {
oneMsg = ""
} else {
for _, v := range msg {
oneMsg += v + "\n"
}
}
color.Printf("\n<red_b>FATAL</>: %s\n%s\nAborting", err.Error(), oneMsg)
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
print(".")
}
print("\n")
errorcheck.ErrorCheck(err, msg...)
}

View file

@ -1,16 +1,19 @@
package configs
import (
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/klauspost/cpuid/v2"
"github.com/HikariKnight/quickpassthrough/internal/common"
"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
@ -70,39 +73,32 @@ func Set_Cmdline(gpu_IDs []string) {
fileio.AppendContent(fmt.Sprintf(" vfio_pci.ids=%s", strings.Join(gpu_IDs, ",")), config.Path.CMDLINE)
}
// Configures systemd-boot using kernelstub
func Set_KernelStub() string {
// Set_KernelStub configures systemd-boot using kernelstub.
func Set_KernelStub(isRoot bool) {
// 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)
// Run and log, check for errors
common.ErrorCheck(
command.ExecAndLogSudo(isRoot, true, "kernelstub", "-a", kernel_args),
"Error, kernelstub command returned exit code 1",
)
}
// Configures grub2 and/or systemd-boot using grubby
func Set_Grubby() string {
// Set_Grubby configures grub2 and/or systemd-boot using grubby
func Set_Grubby(isRoot bool) 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")
// Run and log, check for errors
err := command.ExecAndLogSudo(isRoot, true, "grubby", "--update-kernel=ALL", fmt.Sprintf("--args=%s", kernel_args))
common.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)
@ -116,8 +112,8 @@ func Configure_Grub2() {
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)
if exists, _ := fileio.FileExist(conffile); exists {
_ = os.Remove(conffile)
}
// Make a regex to get the system path instead of the config path
@ -201,8 +197,8 @@ func clean_Grub2_Args(old_kernel_args []string) []string {
return clean_kernel_args
}
// This function copies our config to /etc/default/grub and updates grub
func Set_Grub2() ([]string, error) {
// Set_Grub2 copies our config to /etc/default/grub and updates grub
func Set_Grub2(isRoot bool) error {
// Get the config
config := GetConfig()
@ -213,38 +209,45 @@ func Set_Grub2() ([]string, error) {
sysfile_re := regexp.MustCompile(`^config`)
sysfile := sysfile_re.ReplaceAllString(conffile, "")
// Write to logger
logger.Printf("Executing command:\nsudo cp -v \"%s\" %s\n", conffile, sysfile)
// [CopyToSystem] will log the operation
// logger.Printf("Executing command:\nsudo cp -v \"%s\" %s\n", conffile, sysfile)
// Make our output slice
var output []string
// Copy files to system
output = append(output, CopyToSystem(conffile, sysfile))
// Copy files to system, logging and error checking is done in the function
CopyToSystem(isRoot, conffile, sysfile)
// Set a variable for the mkconfig command
mkconfig := "grub-mkconfig"
var mkconfig string
var grubPath = "/boot/grub/grub.cfg"
var lpErr error
// 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"
mkconfig, lpErr = exec.LookPath("grub-mkconfig")
switch {
case errors.Is(lpErr, exec.ErrNotFound) || mkconfig == "":
// Check for grub2-mkconfig
mkconfig, lpErr = exec.LookPath("grub2-mkconfig")
if lpErr == nil && mkconfig != "" {
grubPath = "/boot/grub2/grub.cfg"
break // skip below, we found grub2-mkconfig
}
if lpErr == nil {
// we know mkconfig is empty despite no error;
// so set an error for [common.ErrorCheck].
lpErr = errors.New("neither grub-mkconfig or grub2-mkconfig found")
}
common.ErrorCheck(lpErr, lpErr.Error()+"\n")
return lpErr // note: unreachable as [common.ErrorCheck] calls fatal
default:
}
// 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")
}
_, mklog, err := command.RunErrSudo(isRoot, mkconfig, "-o", grubPath)
return output, err
// tabulate the output, [command.RunErrSudo] logged the execution.
logger.Printf("\t" + strings.Join(mklog, "\n\t"))
common.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg")
// always returns nil as [common.ErrorCheck] calls fatal
// keeping the ret signature, as we should consider passing down errors
// but that's a massive rabbit hole to go down for this codebase as a whole
return err
}

View file

@ -9,7 +9,7 @@ import (
"github.com/HikariKnight/quickpassthrough/pkg/fileio"
)
// This function writes a dracut configuration file for /etc/dracut.conf.d/
// Set_Dracut writes a dracut configuration file for `/etc/dracut.conf.d/`.
func Set_Dracut() {
config := GetConfig()
@ -17,23 +17,23 @@ func Set_Dracut() {
dracutConf := fmt.Sprintf("%s/vfio.conf", config.Path.DRACUT)
// If the file already exists then delete it
if fileio.FileExist(dracutConf) {
os.Remove(dracutConf)
if exists, _ := fileio.FileExist(dracutConf); exists {
_ = os.Remove(dracutConf)
}
// Write to logger
logger.Printf("Writing to %s:\nadd_drivers+=\" %s \"\n", dracutConf, strings.Join(vfio_modules(), " "))
logger.Printf("Writing to %s:\nforce_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)
fileio.AppendContent(fmt.Sprintf("force_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") {
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)
fileio.AppendContent(" rd.driver.pre=vfio-pci", config.Path.CMDLINE)
}
// Make a backup of dracutConf if there is one there

View file

@ -7,7 +7,7 @@ import (
"regexp"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/pkg/fileio"
)
@ -15,7 +15,7 @@ import (
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))
common.ErrorCheck(err, fmt.Sprintf("Error opening %s", fileName))
defer f.Close()
header_re := regexp.MustCompile(`^#`)
@ -50,7 +50,7 @@ func initramfs_addModules(conffile string) {
// Open the system file for reading
sysfile, err := os.Open(syspath)
errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening file for reading %s", syspath))
common.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

View file

@ -10,14 +10,14 @@ import (
"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
// Set_Mkinitcpio 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)
if exists, _ := fileio.FileExist(config.Path.MKINITCPIO); exists {
_ = os.Remove(config.Path.MKINITCPIO)
}
// Make a regex to get the system path instead of the config path

View file

@ -30,9 +30,9 @@ func Set_Modprobe(gpu_IDs []string) {
conffile := fmt.Sprintf("%s/vfio.conf", config.Path.MODPROBE)
// If the file exists
if fileio.FileExist(conffile) {
if exists, _ := fileio.FileExist(conffile); exists {
// Delete the old file
os.Remove(conffile)
_ = os.Remove(conffile)
}
content := fmt.Sprint(

View file

@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/internal/logger"
)
@ -28,7 +28,7 @@ func GenerateVBIOSDumper(vbios_path string) {
vbios_script_template := fmt.Sprint(
"#!/bin/bash\n",
"# THIS FILE IS AUTO GENERATED!\n",
"# IF YOU HAVE CHANGED GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n",
"# IF YOU HAVE CHANGED THE GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n",
"mkdir -p \"%s\"\n",
"echo Attempting to enable reading from rom\n",
"echo 1 | sudo tee %s\n",
@ -43,24 +43,24 @@ func GenerateVBIOSDumper(vbios_path string) {
vbios_script := fmt.Sprintf(
vbios_script_template,
config.Path.QUICKEMU,
config.Path.QEMU,
vbios_path,
vbios_path,
scriptdir,
config.Path.QUICKEMU,
config.Path.QEMU,
scriptdir,
config.Path.QUICKEMU,
config.Path.QEMU,
vbios_path,
)
// Make the script file
scriptfile, err := os.Create("utils/dump_vbios.sh")
errorcheck.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"")
common.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\"")
common.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"")
// Write to logger
logger.Printf("Writing utils/dump_vbios.sh\n")

View file

@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/internal/logger"
"github.com/HikariKnight/quickpassthrough/pkg/fileio"
)
@ -26,7 +26,7 @@ func DisableVFIOVideo(i int) {
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))
common.ErrorCheck(err, fmt.Sprintf("Could not rewrite %s", config.Path.CMDLINE))
// Enable or disable the VGA based on our given value
if i == 0 {

View file

@ -1,16 +1,19 @@
package configs
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/klauspost/cpuid/v2"
"github.com/HikariKnight/quickpassthrough/internal/common"
"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 {
@ -19,7 +22,7 @@ type Path struct {
INITRAMFS string
ETCMODULES string
DEFAULT string
QUICKEMU string
QEMU string
DRACUT string
MKINITCPIO string
}
@ -30,9 +33,10 @@ type Config struct {
Path *Path
Gpu_Group string
Gpu_IDs []string
IsRoot bool
}
// Gets the path to all the config files
// GetConfigPaths retrieves the path to all the config files.
func GetConfigPaths() *Path {
Paths := &Path{
CMDLINE: "config/kernel_args",
@ -40,7 +44,7 @@ func GetConfigPaths() *Path {
INITRAMFS: "config/etc/initramfs-tools",
ETCMODULES: "config/etc/modules",
DEFAULT: "config/etc/default",
QUICKEMU: "config/quickemu",
QEMU: "config/qemu",
DRACUT: "config/etc/dracut.conf.d",
MKINITCPIO: "config/etc/mkinitcpio.conf",
}
@ -48,7 +52,7 @@ func GetConfigPaths() *Path {
return Paths
}
// Gets all the configs and returns the struct
// GetConfig retrieves all the configs and returns the struct.
func GetConfig() *Config {
config := &Config{
Bootloader: "unknown",
@ -64,7 +68,7 @@ func GetConfig() *Config {
return config
}
// Constructs the empty config files and folders based on what exists on the system
// InitConfigs constructs the empty config files and folders based on what exists on the system
func InitConfigs() {
config := GetConfig()
@ -77,10 +81,17 @@ func InitConfigs() {
}
// Remove old config
os.RemoveAll("config")
if err := os.RemoveAll("config"); err != nil && !errors.Is(err, os.ErrNotExist) {
// won't be called if the error is ErrNotExist
common.ErrorCheck(err, "\nError removing old config")
}
// Make the config folder
os.Mkdir("config", os.ModePerm)
if err := os.Mkdir("config", os.ModePerm); err != nil && !errors.Is(err, os.ErrExist) {
// won't be called if the error is ErrExist
common.ErrorCheck(err, "\nError making config folder")
}
// Make a regex to get the system path instead of the config path
syspath_re := regexp.MustCompile(`^config`)
@ -90,8 +101,16 @@ func InitConfigs() {
// Get the system path
syspath := syspath_re.ReplaceAllString(confpath, "")
exists, err := fileio.FileExist(syspath)
// If we received an error that is not ErrNotExist
if err != nil {
common.ErrorCheck(err, "\nError checking for directory: "+syspath)
continue // note: unreachable due to ErrorCheck calling fatal
}
// If the path exists
if fileio.FileExist(syspath) {
if exists {
// Write to log
logger.Printf(
"%s found on the system\n"+
@ -104,8 +123,10 @@ func InitConfigs() {
makeBackupDir(syspath)
// Create the directories for our configs
err := os.MkdirAll(confpath, os.ModePerm)
errorcheck.ErrorCheck(err)
if err = os.MkdirAll(confpath, os.ModePerm); err != nil && !errors.Is(err, os.ErrExist) {
common.ErrorCheck(err, "\nError making directory: "+confpath)
return // note: unreachable due to ErrorCheck calling fatal
}
}
}
@ -128,7 +149,15 @@ func InitConfigs() {
sysfile := syspath_re.ReplaceAllString(conffile, "")
// If the file exists
if fileio.FileExist(sysfile) {
exists, err := fileio.FileExist(sysfile)
// If we received an error that is not ErrNotExist
if err != nil {
common.ErrorCheck(err, "\nError checking for file: "+sysfile)
continue // note: unreachable due to ErrorCheck calling fatal
}
if exists {
// Write to log
logger.Printf(
"%s found on the system\n"+
@ -139,16 +168,22 @@ func InitConfigs() {
// Create the directories for our configs
file, err := os.Create(conffile)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
// Close the file so we can edit it
file.Close()
_ = file.Close()
// Backup the sysfile if we do not have a backup
backupFile(sysfile)
}
exists, err = fileio.FileExist(conffile)
if err != nil {
common.ErrorCheck(err, "\nError checking for file: "+conffile)
continue // note: unreachable
}
// If we now have a config that exists
if fileio.FileExist(conffile) {
if exists {
switch conffile {
case config.Path.ETCMODULES:
// Write to logger
@ -186,7 +221,7 @@ func vfio_modules() []string {
// If we are on a kernel older than 6.2
sysinfo := uname.New()
kernel_re := regexp.MustCompile(`^(6\.1|6\.0|[1-5]\.)`)
kernel_re := regexp.MustCompile(`^(6\.1|6\.0|[1-5]\.\d{1,2})\.`)
if kernel_re.MatchString(sysinfo.Kernel) {
// Write to the debug log
logger.Printf("Linux kernel version %s detected!\nIncluding vfio_virqfd module\n", sysinfo.Kernel)
@ -204,14 +239,28 @@ func backupFile(source string) {
// Make a destination path
dest := fmt.Sprintf("backup%s", source)
configExists, configFileError := fileio.FileExist(fmt.Sprintf("config%s", source))
sysExists, sysFileError := fileio.FileExist(source)
destExists, destFileError := fileio.FileExist(dest)
// If we received an error that is not ErrNotExist on any of the files
for _, err := range []error{configFileError, sysFileError, destFileError} {
if err != nil {
common.ErrorCheck(configFileError, "\nError checking for file: "+source)
return // note: unreachable
}
}
switch {
// 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) {
case configExists && !sysExists:
// 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) {
common.ErrorCheck(err, "Error creating file %s\n", dest)
_ = file.Close()
// If a backup of the file does not exist
case sysExists && !destExists:
// Write to the logger
logger.Printf("No first time backup of %s detected.\nCreating a backup at %s\n", source, dest)
@ -223,29 +272,64 @@ func backupFile(source string) {
func makeBackupDir(dest string) {
// If a backup directory does not exist
if !fileio.FileExist("backup/") {
exists, err := fileio.FileExist("backup/")
if err != nil {
// If we received an error that is not ErrNotExist
common.ErrorCheck(err, "Error checking for backup/ folder")
return // note: unreachable
}
if !exists {
// 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")
if err = os.MkdirAll(fmt.Sprintf("backup/%s", dest), os.ModePerm); errors.Is(err, os.ErrExist) {
// ignore if the directory already exists
err = nil
}
// will return without incident if there's no error
common.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 {
// CopyToSystem copies a file to the system.
func CopyToSystem(isRoot bool, conffile, sysfile 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], "")
// ExecAndLogSudo will write to the logger, so just print here
fmt.Printf("Copying: %s to %s\n", conffile, sysfile)
// Write output to logger
logger.Printf("%s\n", clean_output)
if isRoot {
logger.Printf("Copying %s to %s\n", conffile, sysfile)
fmt.Printf("Copying %s to %s\n", conffile, sysfile)
fDat, err := os.ReadFile(conffile)
common.ErrorCheck(err, fmt.Sprintf("Failed to read %s", conffile))
err = os.WriteFile(sysfile, fDat, 0644)
common.ErrorCheck(err, fmt.Sprintf("Failed to write %s", sysfile))
logger.Printf("Copied %s to %s\n", conffile, sysfile)
return
}
// Return the output
return fmt.Sprintf("Copying: %s", clean_output)
if !filepath.IsAbs(conffile) {
conffile, _ = filepath.Abs(conffile)
}
err := command.ExecAndLogSudo(isRoot, false, "cp", "-v", conffile, sysfile)
errMsg := ""
if err != nil {
errMsg = err.Error()
}
// [command.ExecAndLogSudo] will log the command's output
common.ErrorCheck(err, fmt.Sprintf("Failed to copy %s to %s:\n%s", conffile, sysfile, errMsg))
// ---------------------------------------------------------------------------------
// note that if we failed the error check, the following will not appear in the log!
// this is because the [common.ErrorCheck] function will call [log.Fatalf] and exit
// ---------------------------------------------------------------------------------
logger.Printf("Copied %s to %s\n", conffile, sysfile)
}

View file

@ -0,0 +1,33 @@
package configs
import (
"os"
"path/filepath"
"testing"
)
func TestCopyToSystem(t *testing.T) {
if err := os.Mkdir("testdir", 0755); err != nil {
t.Fatal(err)
}
tFilePath := filepath.Join("testdir", "testfile")
if err := os.WriteFile(tFilePath, []byte("test"), 0644); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.RemoveAll("testdir"); err != nil {
t.Fatal(err)
}
})
isRoot := os.Getuid() == 0
switch isRoot {
case true:
t.Run("TestCopyToSystem_AsRoot", func(t *testing.T) {
CopyToSystem(true, tFilePath, "/etc/testfile")
})
default:
t.Run("TestCopyToSystem_AsUser", func(t *testing.T) {
CopyToSystem(false, tFilePath, "/etc/testfile")
})
}
}

View file

@ -12,10 +12,11 @@ import (
"strings"
"time"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/cavaliergopher/grab/v3"
"github.com/HikariKnight/quickpassthrough/internal/common"
"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/
@ -95,14 +96,14 @@ type Response struct {
func CheckLsIOMMU() {
// Check the API for releases
resp, err := http.Get("https://api.github.com/repos/hikariknight/ls-iommu/releases/latest")
errorcheck.ErrorCheck(err)
common.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)
common.ErrorCheck(err)
var result Response
if err := json.Unmarshal(body, &result); err != nil {
@ -111,9 +112,9 @@ func CheckLsIOMMU() {
// Make the directory for ls-iommu if it does not exist
path := "utils"
if !fileio.FileExist(path) {
if exists, _ := fileio.FileExist(path); !exists {
err := os.Mkdir(path, os.ModePerm)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
}
// Generate the download url
@ -124,7 +125,8 @@ func CheckLsIOMMU() {
// Generate checksums.txt url
checkSumsUrl := fmt.Sprintf(
"https://github.com/HikariKnight/ls-iommu/releases/download/%s/checksums.txt",
"https://github.com/HikariKnight/ls-iommu/releases/download/%s/ls-iommu_%s_checksums.txt",
result.TagName,
result.TagName,
)
@ -132,30 +134,30 @@ func CheckLsIOMMU() {
// Get the checksum data
checksums, err := http.Get(checkSumsUrl)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
defer checksums.Body.Close()
checksums_txt, err := io.ReadAll(checksums.Body)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
// Check if the tar.gz exists
if !fileio.FileExist(fileName) {
if exists, _ := fileio.FileExist(fileName); !exists {
downloadNewVersion(path, fileName, downloadUrl)
if checkSum(string(checksums_txt), fileName) {
err = untar.Untar(fmt.Sprintf("%s/", path), fileName)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
}
} else {
if !checkSum(string(checksums_txt), fileName) {
downloadNewVersion(path, fileName, downloadUrl)
err = untar.Untar(fmt.Sprintf("%s/", path), fileName)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
}
}
}
func checkSum(checksums string, fileName string) bool {
r, err := os.Open(fileName)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
defer r.Close()
hasher := sha256.New()
@ -181,7 +183,7 @@ func downloadNewVersion(path, fileName, downloadUrl string) {
// check for errors
if err := download.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Download failed: %v\n", err)
if !fileio.FileExist("utils/ls-iommu") {
if exists, _ := fileio.FileExist("utils/ls-iommu"); !exists {
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.")

View file

@ -4,13 +4,14 @@ import (
"fmt"
"os"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/gookit/color"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/internal/configs"
lsiommu "github.com/HikariKnight/quickpassthrough/internal/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) {
@ -94,10 +95,10 @@ func viewGPU(config *configs.Config, ext ...int) {
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) {
if exists, _ := fileio.FileExist(config.Path.CMDLINE); exists {
// 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))
common.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE))
}
// Write initial kernel_arg file
@ -114,10 +115,10 @@ func viewGPU(config *configs.Config, ext ...int) {
)
// If the kernel_args file already exists
if fileio.FileExist(config.Path.CMDLINE) {
if exists, _ := fileio.FileExist(config.Path.CMDLINE); exists {
// 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))
common.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE))
}
// Write initial kernel_arg file

View file

@ -26,21 +26,34 @@ func genVBIOS_dumper(config *configs.Config) {
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)
// Search for a vbios path and generate the vbios dumping script if found
vbios_paths := lsiommu.GetIOMMU("-g", "-i", config.Gpu_Group, "--rom")
if len(vbios_paths) != 0 {
configs.GenerateVBIOSDumper(vbios_paths[0])
}
// Make the qemu config folder
os.Mkdir(fmt.Sprintf("%s/%s", scriptdir, config.Path.QEMU), os.ModePerm)
// Generate a dummy rom (1MB rom of zeroes) for use with AMD RX 7000 series cards by recommendation from Gnif
// Source: https://forum.level1techs.com/t/the-state-of-amd-rx-7000-series-vfio-passthrough-april-2024/210242
command.Run("dd", "if=/dev/zero", fmt.Sprintf("of=%s/%s/dummy.rom", scriptdir, config.Path.QEMU), "bs=1M", "count=1")
// Write a title
title := color.New(color.BgHiBlue, color.White, color.Bold)
title.Println("Generated \"dump VBIOS\" script")
title.Println("VBIOS roms for Passthrough")
// Tell users about the VBIOS dumper script
// Tell users about the VBIOS dumper script and dummy rom for RX 7000 series cards
fmt.Print(
"For some GPUs, you will need to dump the VBIOS and pass the\n",
"If you have an RX 7000 series (and possibly newer AMD cards) GPUs, please use the dummy.rom file\n",
fmt.Sprintf("%s/%s/dummy.rom\n", scriptdir, config.Path.QEMU),
"Or disable ROM BAR for the card in qemu/libvirt\n",
"\n",
"For some other GPUs, you will need to instead dump the VBIOS (and possibly patch it) 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",
"If we found a romfile for your GPU you can also attempt to dump your own vbios from TTY using the script in\n",
fmt.Sprintf("%s/utils/dump_vbios.sh\n", scriptdir),
"\n",
)

View file

@ -4,11 +4,12 @@ import (
"fmt"
"os"
"github.com/gookit/color"
"github.com/HikariKnight/quickpassthrough/internal/configs"
lsiommu "github.com/HikariKnight/quickpassthrough/internal/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) {

View file

@ -6,34 +6,34 @@ import (
"log"
"os"
"os/user"
"strings"
"syscall"
"github.com/gookit/color"
"golang.org/x/term"
"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) {
if exists, _ := fileio.FileExist(config.Path.MODPROBE); exists {
// Configure modprobe
configs.Set_Modprobe(config.Gpu_IDs)
}
// If we have a folder for dracut
if fileio.FileExist(config.Path.DRACUT) {
if exists, _ := fileio.FileExist(config.Path.DRACUT); exists {
// Configure dracut
configs.Set_Dracut()
}
// If we have a mkinitcpio.conf file
if fileio.FileExist(config.Path.MKINITCPIO) {
if exists, _ := fileio.FileExist(config.Path.MKINITCPIO); exists {
configs.Set_Mkinitcpio()
}
@ -48,6 +48,43 @@ func prepModules(config *configs.Config) {
finalize(config)
}
func finalizeNotice(isRoot bool) {
color.Print(`
The configuration files have been generated and are located inside the "config" folder
* The "kernel_args" file contains kernel arguments that your bootloader needs
* The "qemu" folder contains files that may be needed for passthrough
* The files inside the "etc" folder must be copied to your system.
<red>Verify that these files are correctly formated/edited!</>
Once all files have been copied, the following steps must be taken:
* bootloader configuration must be updated
* initramfs must be rebuilt
`)
switch isRoot {
case true:
color.Print("This program can do this for you, if desired.\n")
default:
color.Print(`This program can do this for you, however your sudo password is required.
To avoid this:
* press CTRL+C and perform the steps mentioned above manually.
OR
* run ` + os.Args[0] + ` as root.
`)
}
color.Print(`
If you want to go back and change something, choose Back.
NOTE: A backup of the original files from the first run can be found in the backup folder
`)
}
func finalize(config *configs.Config) {
// Clear the screen
command.Clear()
@ -56,60 +93,43 @@ func finalize(config *configs.Config) {
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",
)
config.IsRoot = os.Getuid() == 0
// 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.")
finalizeNotice(config.IsRoot)
// Parse the choice
switch choice {
// Make a choice of going next or back and parse the choice
switch menu.Next("Press Next to continue with sudo using STDIN, ESC to exit or Back to go back.") {
case "next":
installPassthrough(config)
case "back":
// Go back
disableVideo(config)
}
}
func installPassthrough(config *configs.Config) {
// Get the user data
user, err := user.Current()
currentUser, 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")
if !config.IsRoot {
// Provide a password prompt
fmt.Printf("[sudo] password for %s: ", currentUser.Username)
bytep, err := term.ReadPassword(syscall.Stdin)
if err != nil {
os.Exit(1)
}
fmt.Print("\n")
// Elevate with sudo
command.Elevate(
base64.StdEncoding.EncodeToString(
bytep,
),
)
// Elevate with sudo
command.Elevate(
base64.StdEncoding.EncodeToString(
bytep,
),
)
}
// Make an output string
var output string
@ -120,22 +140,24 @@ func installPassthrough(config *configs.Config) {
logger.Printf("Configuring systemd-boot using kernelstub\n")
// Configure kernelstub
output = configs.Set_KernelStub()
fmt.Printf("%s\n", output)
// callee logs the output and checks for errors
configs.Set_KernelStub(config.IsRoot)
} else if config.Bootloader == "grubby" {
// Write to logger
logger.Printf("Configuring bootloader using grubby\n")
// Configure kernelstub
output = configs.Set_Grubby()
output = configs.Set_Grubby(config.IsRoot)
fmt.Printf("%s\n", output)
} else if config.Bootloader == "grub2" {
// Write to logger
logger.Printf("Applying grub2 changes\n")
grub_output, _ := configs.Set_Grub2()
fmt.Printf("%s\n", strings.Join(grub_output, "\n"))
_ = configs.Set_Grub2(config.IsRoot) // note: we set config.IsRoot earlier
// we'll print the output in the [configs.Set_Grub2] method
// fmt.Printf("%s\n", strings.Join(grub_output, "\n"))
} else {
kernel_args := fileio.ReadFile(config.Path.CMDLINE)
@ -145,68 +167,62 @@ func installPassthrough(config *configs.Config) {
// 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)
// lets hope by now we've already handled any permissions issues...
// TODO: verify that we actually can drop the errors on [fileio.FileExist] call below
if exists, _ := fileio.FileExist(modprobeFile); exists {
// Copy initramfs-tools module to system, note that CopyToSystem will log the command and output
// as well as check for errors
configs.CopyToSystem(config.IsRoot, modprobeFile, "/etc/modprobe.d/vfio.conf")
}
// 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) {
initramFsExists, initramFsErr := fileio.FileExist(initramfsFile)
dracutExists, dracutErr := fileio.FileExist(dracutFile)
mkinitcpioExists, mkinitcpioErr := fileio.FileExist(config.Path.MKINITCPIO)
for _, err = range []error{initramFsErr, dracutErr, mkinitcpioErr} {
if err == nil {
continue
}
// we know this error isn't ErrNotExist, so we should throw it and exit
log.Fatalf("Failed to stat file: %s", err)
}
switch {
case initramFsExists:
// Copy initramfs-tools module to system
output = configs.CopyToSystem(initramfsFile, "/etc/initramfs-tools/modules")
fmt.Printf("%s\n", output)
configs.CopyToSystem(config.IsRoot, initramfsFile, "/etc/initramfs-tools/modules")
// Copy the modules file to /etc/modules
output = configs.CopyToSystem(config.Path.ETCMODULES, "/etc/modules")
fmt.Printf("%s\n", output)
configs.CopyToSystem(config.IsRoot, config.Path.ETCMODULES, "/etc/modules")
// Write to logger
logger.Printf("Executing: sudo update-initramfs -u\n")
if err = command.ExecAndLogSudo(config.IsRoot, true, "update-initramfs", "-u"); err != nil {
log.Fatalf("Failed to update initramfs: %s", err)
}
// 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) {
case dracutExists:
// Copy dracut config to /etc/dracut.conf.d/vfio
output = configs.CopyToSystem(dracutFile, "/etc/dracut.conf.d/vfio")
fmt.Printf("%s\n", output)
configs.CopyToSystem(config.IsRoot, dracutFile, "/etc/dracut.conf.d/vfio")
// Get systeminfo
sysinfo := uname.New()
// Write to logger
logger.Printf("Executing: sudo dracut -f -v --kver %s\n", sysinfo.Release)
if err = command.ExecAndLogSudo(config.IsRoot, true, "dracut", "-f", "-v", "--kver", sysinfo.Release); err != nil {
log.Fatalf("Failed to update initramfs: %s", err)
}
// 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) {
case mkinitcpioExists:
// Copy dracut config to /etc/dracut.conf.d/vfio
output = configs.CopyToSystem(config.Path.MKINITCPIO, "/etc/mkinitcpio.conf")
fmt.Printf("%s\n", output)
configs.CopyToSystem(config.IsRoot, config.Path.MKINITCPIO, "/etc/mkinitcpio.conf")
// 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"))
if err = command.ExecAndLogSudo(config.IsRoot, true, "mkinitcpio", "-P"); err != nil {
log.Fatalf("Failed to update initramfs: %s", err)
}
}
// Make sure prompt end up on next line

View file

@ -0,0 +1,23 @@
package pages
import (
"strings"
"testing"
)
func TestFinalizeNotice(t *testing.T) {
msg := "\n%s\nprinting the finalize notice for manual review, this test should always pass.\n%s\n\n"
divider := strings.Repeat("-", len(msg)-12)
t.Logf(msg, divider, divider)
t.Log("\n\nWith isRoot == true:\n\n")
finalizeNotice(true)
println("\n\n")
t.Log("\n\nWith isRoot == false:\n\n")
finalizeNotice(false)
println("\n\n")
}

View file

@ -6,17 +6,18 @@ package internal
import (
"os"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/pages"
tea "github.com/charmbracelet/bubbletea"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/internal/pages"
)
// 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")
_ = os.Rename("quickpassthrough_debug.log", "quickpassthrough_debug_old.log")
logfile, err := tea.LogToFile("quickpassthrough_debug.log", "")
common.ErrorCheck(err, "Error creating log file")
defer logfile.Close()
// New WIP Tui

View file

@ -3,14 +3,18 @@ package command
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
"github.com/HikariKnight/quickpassthrough/internal/logger"
)
// Run a command and return STDOUT
func Run(binary string, args ...string) ([]string, error) {
var stdout, stderr bytes.Buffer
@ -26,14 +30,14 @@ func Run(binary string, args ...string) ([]string, error) {
output, _ := io.ReadAll(&stdout)
// Get the output
outputs := []string{}
outputs := make([]string, 0, 1)
outputs = append(outputs, string(output))
// Return our list of items
return outputs, err
}
// This function is just like command.Run() but also returns STDERR
// RunErr is just like command.Run() but also returns STDERR
func RunErr(binary string, args ...string) ([]string, []string, error) {
var stdout, stderr bytes.Buffer
@ -58,8 +62,18 @@ func RunErr(binary string, args ...string) ([]string, []string, error) {
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
func RunErrSudo(isRoot bool, binary string, args ...string) ([]string, []string, error) {
if !isRoot && binary != "sudo" {
args = append([]string{binary}, args...)
binary = "sudo"
}
logger.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
fmt.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
return RunErr(binary, args...)
}
// Elevate elevates 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
@ -70,29 +84,83 @@ func Elevate(password string) {
// Open STDIN
stdin, err := cmd.StdinPipe()
errorcheck.ErrorCheck(err, "\nFailed to get sudo STDIN")
common.ErrorCheck(err, "\nFailed to get sudo STDIN")
// Start the authentication
cmd.Start()
err = cmd.Start()
common.ErrorCheck(err, "\nFailed to start sudo command")
// Get the passed password
pw, _ := base64.StdEncoding.DecodeString(password)
_, err = stdin.Write([]byte(string(pw) + "\n"))
errorcheck.ErrorCheck(err, "\nFailed at typing to STDIN")
common.ErrorCheck(err, "\nFailed at typing to STDIN")
// Clear the password
pw = nil
password = ""
stdin.Close()
_ = 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")
common.ErrorCheck(err, "\nError, password given was wrong")
}
// Function to just clear the terminal
// Clear clears the terminal.
func Clear() {
c := exec.Command("clear")
c.Stdout = os.Stdout
c.Run()
_ = c.Run()
}
// ExecAndLogSudo executes an elevated command and logs the output.
//
// * if we're root, the command is executed directly
// * if we're not root, the command is prefixed with "sudo"
//
// - noisy determines if we should print the command to the user
// noisy isn't set to true by our copy caller, as it logs differently,
// but other callers set it.
func ExecAndLogSudo(isRoot, noisy bool, exe string, args ...string) error {
if !isRoot && exe != "sudo" {
og := exe
exe = "sudo"
newArgs := make([]string, 0)
newArgs = append(newArgs, og)
newArgs = append(newArgs, args...)
args = newArgs
}
// Write to logger
logger.Printf("Executing (elevated): %s %s\n", exe, strings.Join(args, " "))
if noisy {
// Print to the user
fmt.Printf("Executing (elevated): %s %s\nSee debug.log for detailed output\n", exe, strings.Join(args, " "))
}
wd, err := os.Getwd()
if err != nil {
return err
}
r := exec.Command(exe, args...)
r.Dir = wd
cmdCombinedOut, err := r.CombinedOutput()
outStr := string(cmdCombinedOut)
// Write to logger, tabulate output
// tabulation denotes it's hierarchy as a child of the command
outStr = strings.ReplaceAll(outStr, "\n", "\n\t")
logger.Printf("\t" + string(cmdCombinedOut) + "\n")
if noisy {
// Print to the user
fmt.Printf("%s\n", outStr)
}
if err != nil {
err = fmt.Errorf("failed to execute %s: %w\n%s", exe, err, outStr)
}
return err
}

View file

@ -0,0 +1,61 @@
package command
import (
"os"
"path/filepath"
"strings"
"testing"
)
const fakeSudo = `#!/bin/sh
"$@" -qptest`
const fakeUtil = `#!/bin/sh
echo "$@"
if [ "$4" = "-qptest" ]; then exit 0; else exit 1; fi`
func setupExecTestEnv(t *testing.T) (string, string) {
t.Helper()
tmpDir := t.TempDir()
fakeSudoPath := filepath.Join(tmpDir, "sudo")
fakeUtilPath := filepath.Join(tmpDir, "util")
if err := os.WriteFile(fakeSudoPath, []byte(fakeSudo), 0755); err != nil {
t.Fatalf("failed to write fake sudo stub: %s", err.Error())
}
if err := os.WriteFile(fakeUtilPath, []byte(fakeUtil), 0755); err != nil {
t.Fatalf("failed to write fake util stub: %s", err.Error())
}
t.Setenv("PATH", tmpDir+":"+os.Getenv("PATH"))
return fakeSudoPath, fakeUtilPath
}
func TestExecAndLogSudo(t *testing.T) {
_, fakeUtilPath := setupExecTestEnv(t)
args := []string{"i am a string with spaces", "i came to ruin parsers and chew bubble gum", "and I'm all out of bubblegum."}
t.Run("is_not_root", func(t *testing.T) {
if err := ExecAndLogSudo(false, false, "util", args...); err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
})
t.Run("is_root", func(t *testing.T) {
newFakeUtil := strings.Replace(fakeUtil, "exit 1", "exit 0", 1)
newFakeUtil = strings.Replace(newFakeUtil, "exit 0", "exit 1", 1)
if err := os.WriteFile(fakeUtilPath, []byte(newFakeUtil), 0755); err != nil {
t.Fatalf("failed to overwrite fake util with modified stub: %s", err.Error())
}
if err := ExecAndLogSudo(false, false, "util", args...); err == nil {
t.Errorf("expected error when using modified util with sudo, got nil")
}
if err := ExecAndLogSudo(true, true, "util", args...); err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
})
}

View file

@ -7,29 +7,31 @@ import (
"io"
"os"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
)
/*
* 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)
// AppendContent 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))
common.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))
common.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName))
}
// Reads the file and returns a stringlist with each line
// ReadLines 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))
common.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName))
defer content.Close()
// Make a list of lines
@ -46,54 +48,55 @@ func ReadLines(fileName string) []string {
}
// Reads a file and returns all the content as a string
// ReadFile 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))
common.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 {
// FileExist checks if a file exists and returns a bool and any error that isn't os.ErrNotExist.
func FileExist(fileName string) (bool, error) {
var exist bool
// Check if the file exists
if _, err := os.Stat(fileName); !errors.Is(err, os.ErrNotExist) {
// Set the value to true
_, err := os.Stat(fileName)
switch {
case err == nil:
exist = true
} else {
// Set the value to false
case errors.Is(err, os.ErrNotExist):
// Set the value to true
exist = false
err = nil
}
// Return if the file exists
return exist
return exist, err
}
// Copies a FILE from source to dest
// FileCopy 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)
common.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)
common.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)
common.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)
common.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile)
}
}

View file

@ -4,8 +4,9 @@ import (
"fmt"
"strings"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/gookit/color"
"github.com/HikariKnight/quickpassthrough/internal/common"
)
func ManualInput(msg string, format string) []string {
@ -18,7 +19,7 @@ func ManualInput(msg string, format string) []string {
// Get the user input
var input string
_, err := fmt.Scan(&input)
errorcheck.ErrorCheck(err)
common.ErrorCheck(err)
input_list := strings.Split(input, ",")

View file

@ -8,7 +8,7 @@ import (
"os"
"path/filepath"
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
"github.com/HikariKnight/quickpassthrough/internal/common"
)
// Slightly modified from source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07
@ -17,7 +17,7 @@ import (
// 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))
common.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName))
defer r.Close()
gzr, err := gzip.NewReader(r)