Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 61 additions & 15 deletions mantle/cmd/kola/devshell.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func displayStatusMsg(ontty bool, status, msg string, termMaxWidth int) {
fmt.Printf("\033[2K\r%s", s)
}

func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *conf.Conf, sshCommand string) error {
func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *conf.Conf, sshCommand string, rootSMBIOS bool) error {
ontty := term.IsTerminal(0)
if sshCommand == "" {
if !ontty {
Expand Down Expand Up @@ -95,8 +95,17 @@ func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *co
return err
}

conf.CopyKeys(keys)
builder.SetConfig(conf)
if rootSMBIOS {
var authorizedKeys string
for _, key := range keys {
authorizedKeys += key.String()
authorizedKeys += "\n"
}
builder.InjectSSHAuthorizedKeysViaSMBIOS(authorizedKeys)
} else {
conf.CopyKeys(keys)
builder.SetConfig(conf)
}

// errChan communicates errors from go routines
errChan := make(chan error)
Expand All @@ -105,8 +114,10 @@ func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *co
// stateChan reports in-instance state such as shutdown, reboot, etc.
stateChan := make(chan guestState)

if err = watchJournal(builder, conf, stateChan, errChan); err != nil {
return err
if conf != nil {
if err = watchJournal(builder, conf, stateChan, errChan); err != nil {
return err
}
}

// SerialPipe is the pipe output from the serial console.
Expand Down Expand Up @@ -186,9 +197,25 @@ func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *co

// Start the SSH client
sc := newSshClient(ip, agent.Socket, sshCommand)
if rootSMBIOS {
sc.user = "root"
}
sc.ontty = ontty
go sc.controlStartStop()

if rootSMBIOS {
go func() {
err := util.Retry(6, 5*time.Second, func() error {
return sc.check()
})
if err != nil {
errChan <- fmt.Errorf("failed to await ssh: %w", err)
} else {
stateChan <- guestStateOpenSshStarted
}
}()
}

ready := false
statusMsg := "STARTUP"
lastMsg := ""
Expand All @@ -213,7 +240,7 @@ func runDevShellSSH(ctx context.Context, builder *platform.QemuBuilder, conf *co
return fmt.Errorf("instance failed in initramfs; try rerunning with --devshell-console")
}
if err != nil {
fmt.Fprintf(os.Stderr, "errchan: %v", err)
return err
}

// monitor the instance state
Expand Down Expand Up @@ -472,6 +499,7 @@ type sshClient struct {
port string
agent string
cmd string
user string
ontty bool
controlChan chan sshControlMessage
errChan chan error
Expand All @@ -492,13 +520,39 @@ func newSshClient(host, agent, cmd string) *sshClient {
host: host,
port: port,
agent: agent,
user: "core",
controlChan: make(chan sshControlMessage),
errChan: make(chan error),
// this could be a []string, but ssh sends it over as a string anyway, so meh...
cmd: cmd,
}
}

// baseArgs returns the basic arguments for the SSH command
func (sc *sshClient) baseArgs() []string {
return []string{
"ssh", "-t",
"-o", "User=" + sc.user,
"-o", "StrictHostKeyChecking=no",
"-o", "CheckHostIP=no",
"-o", "IdentityAgent=" + sc.agent,
"-o", "PreferredAuthentications=publickey",
"-p", sc.port, sc.host,
}
}

// check queries whether we can successfully execute a command on the remote system
func (sc *sshClient) check() error {
sshArgs := sc.baseArgs()
// Relatively fast timeout to ensure we don't get stuck by firewalling, etc.
sshArgs = append(sshArgs, "-o", "ConnectTimeout=5", "--", "true")
sshCmd := exec.Command(sshArgs[0], sshArgs[1:]...)
sshCmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
}
return sshCmd.Run()
}

// start starts the SSH session and returns the error over the errChan.
// While the pure-go SSH client would be nicer, the golang SSH client
// doesn't handle some keyboard keys well (arrows, home, end, etc).
Expand All @@ -517,15 +571,7 @@ func (sc *sshClient) start() {
time.Sleep(1 * time.Second)
}()

sshArgs := []string{
"ssh", "-t",
"-o", "User=core",
"-o", "StrictHostKeyChecking=no",
"-o", "CheckHostIP=no",
"-o", "IdentityAgent=" + sc.agent,
"-o", "PreferredAuthentications=publickey",
"-p", sc.port, sc.host,
}
sshArgs := sc.baseArgs()
if sc.cmd != "" {
sshArgs = append(sshArgs, "--", sc.cmd)
}
Expand Down
16 changes: 13 additions & 3 deletions mantle/cmd/kola/qemuexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ var (
bindro []string
bindrw []string

rootSMBIOS bool

directIgnition bool
forceConfigInjection bool
propagateInitramfsFailure bool
Expand Down Expand Up @@ -101,6 +103,7 @@ func init() {
cmdQemuExec.Flags().BoolVar(&propagateInitramfsFailure, "propagate-initramfs-failure", false, "Error out if the system fails in the initramfs")
cmdQemuExec.Flags().StringVarP(&consoleFile, "console-to-file", "", "", "Filepath in which to save serial console logs")
cmdQemuExec.Flags().IntVarP(&additionalNics, "additional-nics", "", 0, "Number of additional NICs to add")
cmdQemuExec.Flags().BoolVarP(&rootSMBIOS, "root-ssh-smbios", "S", false, "Inject root login via systemd credentials (SMBIOS), not Ignition")
cmdQemuExec.Flags().StringVarP(&sshCommand, "ssh-command", "x", "", "Command to execute instead of spawning a shell")
cmdQemuExec.Flags().StringVarP(&netboot, "netboot", "", "", "Filepath to BOOTP program (e.g. PXELINUX/GRUB binary or iPXE script")
cmdQemuExec.Flags().StringVarP(&netbootDir, "netboot-dir", "", "", "Directory to serve over TFTP (default: BOOTP parent dir). If specified, --netboot is relative to this dir.")
Expand Down Expand Up @@ -216,10 +219,12 @@ func runQemuExec(cmd *cobra.Command, args []string) error {
if kola.QEMUOptions.DiskImage == "" && kolaPlatform == "qemu" {
return fmt.Errorf("No disk image provided")
}
ignitionFragments = append(ignitionFragments, "autologin")
if !rootSMBIOS {
ignitionFragments = append(ignitionFragments, "autologin")
}
cpuCountHost = true
usernet = true
if kola.Options.CosaWorkdir != "" {
if !rootSMBIOS && kola.Options.CosaWorkdir != "" {
// Conservatively bind readonly to avoid anything in the guest (stray tests, whatever)
// from destroying stuff
bindro = append(bindro, fmt.Sprintf("%s,/var/mnt/workdir", kola.Options.CosaWorkdir))
Expand Down Expand Up @@ -387,7 +392,12 @@ func runQemuExec(cmd *cobra.Command, args []string) error {
}

if devshell && !devshellConsole {
return runDevShellSSH(ctx, builder, config, sshCommand)
if rootSMBIOS {
if config != nil {
return fmt.Errorf("cannot perform Ignition configuration when using plain SMBIOS")
}
}
return runDevShellSSH(ctx, builder, config, sshCommand, rootSMBIOS)
}
if config != nil {
if directIgnition {
Expand Down
39 changes: 39 additions & 0 deletions mantle/platform/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"bufio"
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"math/rand"
Expand Down Expand Up @@ -477,6 +478,10 @@ type QemuBuilder struct {
ignitionSet bool
ignitionRendered bool

// sshAuthorizedKeysForRootViaSMBIOS is a public SSH key that will be rendered as a credential (https://systemd.io/CREDENTIALS)
// if set.
sshAuthorizedKeysForRootViaSMBIOS string

UsermodeNetworking bool
usermodeNetworkingAddr string
RestrictNetworking bool
Expand Down Expand Up @@ -575,6 +580,28 @@ func (builder *QemuBuilder) renderIgnition() error {
return nil
}

// encodedSystemdTmpfilesForSSH implements the bit from systemd.io/CREDENTIALS
// to inject a public SSH key into authorized_keys for the target user.
func encodedSystemdTmpfilesForSSH(user, pubKey string) (string, error) {
pubKeyEnc := base64.StdEncoding.EncodeToString([]byte(pubKey))

userHomeDir := "/root"
if user != "root" {
userHomeDir = filepath.Join("/home", user)
}

tmpFileCmd := fmt.Sprintf("d %[1]s/.ssh 0700 %[2]s %[2]s -\nf+~ %[1]s/.ssh/authorized_keys 600 %[2]s %[2]s - %[3]s", userHomeDir, user, pubKeyEnc)

tmpFileCmdEnc := base64.StdEncoding.EncodeToString([]byte(tmpFileCmd))
return tmpFileCmdEnc, nil
}

// InjectSSHAuthorizedKeysViaSMBIOS writes the `authorized_keys` file for the `root` user
// injected via SMBIOS and systemd credentials.
func (builder *QemuBuilder) InjectSSHAuthorizedKeysViaSMBIOS(authorizedKeys string) {
builder.sshAuthorizedKeysForRootViaSMBIOS = authorizedKeys
}

// AddFd appends a file descriptor that will be passed to qemu,
// returning a "/dev/fdset/<num>" argument that one can use with e.g.
// -drive file=/dev/fdset/<num>.
Expand Down Expand Up @@ -1071,6 +1098,8 @@ func (disk *Disk) prepare(builder *QemuBuilder) error {
// on our own.
if strings.HasSuffix(backingFile, "qcow2") {
format = "qcow2"
} else if strings.HasSuffix(backingFile, "raw") {
format = "raw"
}
}
if format != "" {
Expand Down Expand Up @@ -1775,6 +1804,16 @@ func (builder *QemuBuilder) Exec() (*QemuInstance, error) {
}
}

if builder.sshAuthorizedKeysForRootViaSMBIOS != "" {
// Right now we hardcode root for simplicity
tmpFilesCmd, err := encodedSystemdTmpfilesForSSH("root", builder.sshAuthorizedKeysForRootViaSMBIOS)
if err != nil {
return nil, err
}
oemString := fmt.Sprintf("type=11,value=io.systemd.credential.binary:tmpfiles.extra=%s", tmpFilesCmd)
argv = append(argv, "-smbios", oemString)
}

// Handle Software TPM
if builder.Swtpm && builder.supportsSwtpm() {
err = builder.ensureTempdir()
Expand Down