Local VM image building
This commit is contained in:
parent
583f443e7e
commit
e7605dc289
13 changed files with 359 additions and 239 deletions
30
cmd/build.go
Normal file
30
cmd/build.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var buildCmd = &cobra.Command{
|
||||
Use: "build",
|
||||
Short: "Build (set up) a VM image for local use. This needs to be run after the initial installation.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
store := createStore()
|
||||
|
||||
err := store.BuildVMImageWithInterruptHandler(vmDebugFlag, buildOverwriteFlag)
|
||||
if err != nil {
|
||||
slog.Error("Failed to build VM image", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("VM image built successfully", "path", store.GetVMImagePath())
|
||||
},
|
||||
}
|
||||
|
||||
var buildOverwriteFlag bool
|
||||
|
||||
func init() {
|
||||
buildCmd.Flags().BoolVar(&buildOverwriteFlag, "overwrite", false, "Specifies whether the VM image should be overwritten with the build.")
|
||||
}
|
||||
11
cmd/clean.go
11
cmd/clean.go
|
|
@ -12,11 +12,12 @@ import (
|
|||
|
||||
var cleanCmd = &cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Remove all downloaded VM images.",
|
||||
Short: "Remove the Linsk data directory.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
store := createStore()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Will delete all VM images in the data directory. Proceed? (y/n) > ")
|
||||
rmPath := store.DataDirPath()
|
||||
fmt.Fprintf(os.Stderr, "Will permanently remove '"+rmPath+"'. Proceed? (y/n) > ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadBytes('\n')
|
||||
|
|
@ -30,12 +31,12 @@ var cleanCmd = &cobra.Command{
|
|||
os.Exit(2)
|
||||
}
|
||||
|
||||
deleted, err := store.CleanImages(false)
|
||||
err = os.RemoveAll(rmPath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to clean images", "error", err.Error())
|
||||
slog.Error("Failed to remove all", "error", err.Error(), "path", rmPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Successful VM image cleanup", "deleted", deleted)
|
||||
slog.Info("Deleted data directory", "path", rmPath)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/AlexSSD7/linsk/utils"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type BuildContext struct {
|
||||
logger *slog.Logger
|
||||
|
||||
vi *vm.VM
|
||||
}
|
||||
|
||||
func NewBuildContext(logger *slog.Logger, baseISOPath string, outPath string, showVMDisplay bool) (*BuildContext, error) {
|
||||
baseISOPath = filepath.Clean(baseISOPath)
|
||||
outPath = filepath.Clean(outPath)
|
||||
|
||||
_, err := os.Stat(outPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.Wrap(err, "stat output file")
|
||||
}
|
||||
|
||||
// File doesn't exist. Going forward with creating a new .qcow2 image.
|
||||
} else {
|
||||
return nil, fmt.Errorf("output file already exists")
|
||||
}
|
||||
|
||||
err = createQEMUImg(outPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create temporary qemu image")
|
||||
}
|
||||
|
||||
vi, err := vm.NewVM(logger.With("subcaller", "vm"), vm.VMConfig{
|
||||
CdromImagePath: baseISOPath,
|
||||
Drives: []vm.DriveConfig{{
|
||||
Path: outPath,
|
||||
}},
|
||||
MemoryAlloc: 512,
|
||||
UnrestrictedNetworking: true,
|
||||
ShowDisplay: showVMDisplay,
|
||||
InstallBaseUtilities: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create vm instance")
|
||||
}
|
||||
|
||||
return &BuildContext{
|
||||
logger: logger,
|
||||
|
||||
vi: vi,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createQEMUImg(outPath string) error {
|
||||
outPath = filepath.Clean(outPath)
|
||||
baseCmd := "qemu-img"
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
baseCmd += ".exe"
|
||||
}
|
||||
|
||||
err := exec.Command(baseCmd, "create", "-f", "qcow2", outPath, "1G").Run()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run qemu-img create cmd")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *BuildContext) BuildWithInterruptHandler() error {
|
||||
defer func() {
|
||||
err := bc.vi.Cancel()
|
||||
if err != nil {
|
||||
bc.logger.Error("Failed to cancel VM context", "error", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
runErrCh := make(chan error, 1)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
defer ctxCancel()
|
||||
|
||||
interrupt := make(chan os.Signal, 2)
|
||||
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Reset()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
err := bc.vi.Run()
|
||||
ctxCancel()
|
||||
runErrCh <- err
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; ; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case sig := <-interrupt:
|
||||
lg := slog.With("signal", sig)
|
||||
|
||||
if i == 0 {
|
||||
lg.Warn("Caught interrupt, safely shutting down")
|
||||
} else if i < 10 {
|
||||
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
|
||||
} else {
|
||||
panic("force interrupt")
|
||||
}
|
||||
|
||||
err := bc.vi.Cancel()
|
||||
if err != nil {
|
||||
lg.Warn("Failed to cancel VM context", "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-runErrCh:
|
||||
if err == nil {
|
||||
return fmt.Errorf("operation canceled by user")
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "start vm")
|
||||
case <-bc.vi.SSHUpNotifyChan():
|
||||
sc, err := bc.vi.DialSSH()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial vm ssh")
|
||||
}
|
||||
|
||||
defer func() { _ = sc.Close() }()
|
||||
|
||||
bc.logger.Info("Installation in progress")
|
||||
|
||||
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd"})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run alpine setup cmd")
|
||||
}
|
||||
|
||||
err = bc.vi.Cancel()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cancel vm context")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-runErrCh:
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run vm")
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error {
|
||||
sess, err := sc.NewSession()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "new session")
|
||||
}
|
||||
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
sess.Stderr = stderr
|
||||
|
||||
defer func() {
|
||||
_ = sess.Close()
|
||||
}()
|
||||
|
||||
cmd := "ifconfig eth0 up && ifconfig lo up && udhcpc && true > /etc/apk/repositories && setup-apkrepos -c -1 && printf 'y' | setup-disk -m sys /dev/vda"
|
||||
|
||||
if len(pkgs) != 0 {
|
||||
pkgsQuoted := make([]string, len(pkgs))
|
||||
for i, rawPkg := range pkgs {
|
||||
pkgsQuoted[i] = shellescape.Quote(rawPkg)
|
||||
}
|
||||
|
||||
cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ")
|
||||
}
|
||||
|
||||
cmd += `&& chroot /mnt ash -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && addgroup -g 1000 linsk && adduser -D -h /mnt -G linsk linsk -u 1000'`
|
||||
|
||||
err = sess.Run(cmd)
|
||||
if err != nil {
|
||||
return utils.WrapErrWithLog(err, "run setup cmd", stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlexSSD7/linsk/cmd/imgbuilder/builder"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "imgbuilder",
|
||||
Short: "Build an Alpine Linux image for Linsk. A base Alpine VM disc image is required.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
baseISOPath := filepath.Clean(args[0])
|
||||
outImagePath := filepath.Clean(args[1])
|
||||
|
||||
bc, err := builder.NewBuildContext(slog.With("caller", "build-context"), baseISOPath, outImagePath, vmDebugFlag)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create a new build context", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = bc.BuildWithInterruptHandler()
|
||||
if err != nil {
|
||||
slog.Error("Failed to build an image", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Success")
|
||||
},
|
||||
}
|
||||
|
||||
var vmDebugFlag bool
|
||||
|
||||
func init() {
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&vmDebugFlag, "vmdebug", false, "Enable VM debug mode. This will open an accessible VM monitor. You can log in with root user and no password.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ func init() {
|
|||
rootCmd.AddCommand(runCmd)
|
||||
rootCmd.AddCommand(shellCmd)
|
||||
rootCmd.AddCommand(cleanCmd)
|
||||
rootCmd.AddCommand(buildCmd)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&vmDebugFlag, "vmdebug", false, "Enables the VM debug mode. This will open an accessible VM monitor. You can log in with root user and no password.")
|
||||
rootCmd.PersistentFlags().BoolVar(&unrestrictedNetworkingFlag, "unrestricted-networking", false, "Enables unrestricted networking. This will allow the VM to connect to the internet.")
|
||||
|
|
|
|||
12
cmd/utils.go
12
cmd/utils.go
|
|
@ -66,9 +66,15 @@ func createStore() *storage.Storage {
|
|||
|
||||
func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool) int {
|
||||
store := createStore()
|
||||
_, err := store.ValidateImageHashOrDownload()
|
||||
|
||||
vmImagePath, err := store.CheckVMImageExists()
|
||||
if err != nil {
|
||||
slog.Error("Failed to validate image hash or download image", "error", err.Error())
|
||||
slog.Error("Failed to check whether VM image exists", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if vmImagePath == "" {
|
||||
slog.Error("VM image does not exist. You need to build it first before attempting to start Linsk. Please run `linsk build` first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +87,7 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
|
||||
vmCfg := vm.VMConfig{
|
||||
Drives: []vm.DriveConfig{{
|
||||
Path: store.GetLocalImagePath(),
|
||||
Path: vmImagePath,
|
||||
SnapshotMode: true,
|
||||
}},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue