Skip to content

Commit da40517

Browse files
cgwaltersopenshift-merge-robot
authored andcommitted
Introduce kola qemuexec --devshell, make cosa run use it
This finally unifies the advantages of `cosa run` and `kola spawn`. I kept getting annoyed by how serial console sizing is broken (e.g. trying to use `less` etc.). Using `ssh` via `kola spawn` addresses that, but it means you can't debug the initramfs. Now things work in an IMO pretty cool way; if you do e.g. `cosa run --kargs ignition.config.url=blah://` (or inject a bad Ignition config) to cause a failure in the initramfs, you'll see a nice error (building on coreos/ignition-dracut#146 ) telling you to rerun with `cosa run --devshell-console`. Things are also wired up cleanly so that we support rebooting with the equivalent of `kola spawn --reconnect` (which we should probably remove now). You can exit via *either* quitting SSH cleanly or using `poweroff`, and the lifecycle of ssh and qemu is wired together. And finally, if we detect a cosa workdir we also bind it in by default. More to come here, such as auto-injecting debugging tools and containers.
1 parent 92dfcf0 commit da40517

File tree

3 files changed

+359
-15
lines changed

3 files changed

+359
-15
lines changed

mantle/cmd/kola/devshell.go

+299
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Copyright 2020 Red Hat, Inc.
2+
//
3+
// Run qemu as a development shell
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package main
18+
19+
import (
20+
"bufio"
21+
"fmt"
22+
"io/ioutil"
23+
"net"
24+
"os"
25+
"os/exec"
26+
"path/filepath"
27+
"strings"
28+
"syscall"
29+
"time"
30+
31+
"github.com/coreos/mantle/util"
32+
"github.com/pkg/errors"
33+
34+
v3 "github.com/coreos/ignition/v2/config/v3_0"
35+
v3types "github.com/coreos/ignition/v2/config/v3_0/types"
36+
37+
"github.com/coreos/mantle/kola"
38+
"github.com/coreos/mantle/platform"
39+
)
40+
41+
const devshellHostname = "cosa-devsh"
42+
43+
func devshellSSH(configPath, keyPath string, silent bool, args ...string) exec.Cmd {
44+
sshArgs := append([]string{"-i", keyPath, "-F", configPath, devshellHostname}, args...)
45+
sshCmd := exec.Command("ssh", sshArgs...)
46+
if !silent {
47+
sshCmd.Stdin = os.Stdin
48+
sshCmd.Stdout = os.Stdout
49+
sshCmd.Stderr = os.Stderr
50+
}
51+
sshCmd.SysProcAttr = &syscall.SysProcAttr{
52+
Pdeathsig: syscall.SIGTERM,
53+
}
54+
55+
return *sshCmd
56+
}
57+
58+
func readTrimmedLine(r *bufio.Reader) (string, error) {
59+
l, err := r.ReadString('\n')
60+
if err != nil {
61+
return "", err
62+
}
63+
return strings.TrimSpace(l), nil
64+
}
65+
66+
func runDevShellSSH(builder *platform.QemuBuilder, conf *v3types.Config) error {
67+
tmpd, err := ioutil.TempDir("", "kola-devshell")
68+
if err != nil {
69+
return err
70+
}
71+
defer os.RemoveAll(tmpd)
72+
73+
sshKeyPath := filepath.Join(tmpd, "ssh.key")
74+
sshPubKeyPath := sshKeyPath + ".pub"
75+
err = exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", sshKeyPath).Run()
76+
if err != nil {
77+
return errors.Wrapf(err, "running ssh-keygen")
78+
}
79+
sshPubKeyBuf, err := ioutil.ReadFile(sshPubKeyPath)
80+
if err != nil {
81+
return errors.Wrapf(err, "reading pubkey")
82+
}
83+
sshPubKey := v3types.SSHAuthorizedKey(strings.TrimSpace(string(sshPubKeyBuf)))
84+
85+
readinessSignalChan := "coreos.devshellready"
86+
signalReadyUnit := fmt.Sprintf(`[Unit]
87+
Requires=dev-virtio\\x2dports-%s.device
88+
OnFailure=emergency.target
89+
OnFailureJobMode=isolate
90+
After=systemd-user-sessions.service
91+
After=sshd.service
92+
[Service]
93+
Type=oneshot
94+
RemainAfterExit=yes
95+
ExecStart=/bin/echo ready
96+
StandardOutput=file:/dev/virtio-ports/%[1]s
97+
[Install]
98+
RequiredBy=multi-user.target`, readinessSignalChan)
99+
signalPoweroffUnit := fmt.Sprintf(`[Unit]
100+
Requires=dev-virtio\\x2dports-%s.device
101+
OnFailure=emergency.target
102+
OnFailureJobMode=isolate
103+
Conflicts=reboot.target
104+
[Service]
105+
Type=oneshot
106+
RemainAfterExit=yes
107+
ExecStop=/bin/echo poweroff
108+
StandardOutput=file:/dev/virtio-ports/%[1]s
109+
[Install]
110+
WantedBy=multi-user.target`, readinessSignalChan)
111+
112+
devshellConfig := v3types.Config{
113+
Ignition: v3types.Ignition{
114+
Version: "3.0.0",
115+
},
116+
Passwd: v3types.Passwd{
117+
Users: []v3types.PasswdUser{
118+
{
119+
Name: "core",
120+
SSHAuthorizedKeys: []v3types.SSHAuthorizedKey{
121+
sshPubKey,
122+
},
123+
},
124+
},
125+
},
126+
Systemd: v3types.Systemd{
127+
Units: []v3types.Unit{
128+
{
129+
Name: "coreos-devshell-signal-ready.service",
130+
Contents: &signalReadyUnit,
131+
Enabled: util.BoolToPtr(true),
132+
},
133+
{
134+
Name: "coreos-devshell-signal-poweroff.service",
135+
Contents: &signalPoweroffUnit,
136+
Enabled: util.BoolToPtr(true),
137+
},
138+
},
139+
},
140+
}
141+
confm := v3.Merge(*conf, devshellConfig)
142+
conf = &confm
143+
144+
readyChan, err := builder.VirtioChannelRead(readinessSignalChan)
145+
if err != nil {
146+
return err
147+
}
148+
readyReader := bufio.NewReader(readyChan)
149+
150+
if err := builder.SetConfig(*conf, kola.Options.IgnitionVersion == "v2"); err != nil {
151+
return errors.Wrapf(err, "rendering config")
152+
}
153+
154+
builder.InheritConsole = false
155+
inst, err := builder.Exec()
156+
if err != nil {
157+
return err
158+
}
159+
defer inst.Destroy()
160+
161+
qemuWaitChan := make(chan error)
162+
errchan := make(chan error)
163+
readychan := make(chan struct{})
164+
go func() {
165+
buf, err := inst.WaitIgnitionError()
166+
if err != nil {
167+
errchan <- err
168+
} else {
169+
// TODO parse buf and try to nicely render something
170+
if buf != "" {
171+
errchan <- platform.ErrInitramfsEmergency
172+
}
173+
}
174+
}()
175+
go func() {
176+
qemuWaitChan <- inst.Wait()
177+
}()
178+
go func() {
179+
readyMsg, err := readTrimmedLine(readyReader)
180+
if err != nil {
181+
errchan <- err
182+
}
183+
if readyMsg != "ready" {
184+
errchan <- fmt.Errorf("Unexpected ready message: %s", readyMsg)
185+
}
186+
var s struct{}
187+
readychan <- s
188+
}()
189+
190+
select {
191+
case err := <-errchan:
192+
if err == platform.ErrInitramfsEmergency {
193+
return fmt.Errorf("instance failed in initramfs; try rerunning with --devshell-console")
194+
}
195+
return err
196+
case err := <-qemuWaitChan:
197+
return errors.Wrapf(err, "qemu exited before setup")
198+
case _ = <-readychan:
199+
fmt.Println("virtio: connected")
200+
}
201+
202+
var ip string
203+
err = util.Retry(6, 5*time.Second, func() error {
204+
var err error
205+
ip, err = inst.SSHAddress()
206+
if err != nil {
207+
return err
208+
}
209+
return nil
210+
})
211+
if err != nil {
212+
return errors.Wrapf(err, "awaiting ssh address")
213+
}
214+
215+
sshConfigPath := filepath.Join(tmpd, "ssh-config")
216+
sshConfig, err := os.OpenFile(sshConfigPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
217+
if err != nil {
218+
return errors.Wrapf(err, "creating ssh config")
219+
}
220+
defer sshConfig.Close()
221+
sshBuf := bufio.NewWriter(sshConfig)
222+
223+
_, err = fmt.Fprintf(sshBuf, "Host %s\n", devshellHostname)
224+
if err != nil {
225+
return err
226+
}
227+
host, port, err := net.SplitHostPort(ip)
228+
if err != nil {
229+
// Yeah this is hacky, surprising there's not a stdlib API for this
230+
host = ip
231+
port = ""
232+
}
233+
if port != "" {
234+
if _, err := fmt.Fprintf(sshBuf, " Port %s\n", port); err != nil {
235+
return err
236+
}
237+
}
238+
if _, err := fmt.Fprintf(sshBuf, ` HostName %s
239+
StrictHostKeyChecking no
240+
UserKnownHostsFile /dev/null
241+
User core
242+
PasswordAuthentication no
243+
KbdInteractiveAuthentication no
244+
GSSAPIAuthentication no
245+
IdentitiesOnly yes
246+
ForwardAgent no
247+
ForwardX11 no
248+
`, host); err != nil {
249+
return err
250+
}
251+
252+
if err := sshBuf.Flush(); err != nil {
253+
return err
254+
}
255+
256+
err = util.Retry(10, 1*time.Second, func() error {
257+
cmd := devshellSSH(sshConfigPath, sshKeyPath, true, "true")
258+
return cmd.Run()
259+
})
260+
if err != nil {
261+
return err
262+
}
263+
264+
poweroffStarted := false
265+
go func() {
266+
msg, _ := readTrimmedLine(readyReader)
267+
if msg == "poweroff" {
268+
poweroffStarted = true
269+
}
270+
}()
271+
272+
go func() {
273+
for {
274+
// FIXME figure out how to differentiate between reboot/shutdown
275+
// if poweroffStarted {
276+
// break
277+
// }
278+
cmd := devshellSSH(sshConfigPath, sshKeyPath, false)
279+
if err := cmd.Run(); err != nil {
280+
fmt.Println("Disconnected, attempting to reconnect (Ctrl-C to exit)")
281+
time.Sleep(1 * time.Second)
282+
} else {
283+
proc := os.Process{
284+
Pid: inst.Pid(),
285+
}
286+
poweroffStarted = true
287+
proc.Signal(os.Interrupt)
288+
break
289+
}
290+
}
291+
}()
292+
err = <-qemuWaitChan
293+
if err == nil {
294+
if !poweroffStarted {
295+
fmt.Println("QEMU powered off unexpectedly")
296+
}
297+
}
298+
return err
299+
}

0 commit comments

Comments
 (0)