Skip to content

Commit 9d5553a

Browse files
committed
refactor(host): use other serial files for Kickstart monitor
Create dedicated serial log files in /tmp for Kickstart instead of using /dev/pts/xx. Using /dev/pts/xx causes the host to receive SIGHUP and stop when the master end of the serial port terminates(VM stop).
1 parent d6be91b commit 9d5553a

File tree

4 files changed

+126
-84
lines changed

4 files changed

+126
-84
lines changed

pkg/hostman/guestman/kickstart_helper.go

Lines changed: 79 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"fmt"
2121
"os"
2222
"path"
23-
"regexp"
2423
"runtime/debug"
2524
"strings"
2625
"time"
@@ -36,91 +35,102 @@ import (
3635
)
3736

3837
const (
39-
KICKSTART_MONITOR_TIMEOUT = 30 * time.Minute
40-
SERIAL_DEVICE_CHECK_INTERVAL = 5 * time.Second
41-
SERIAL_DEVICE_CHECK_MAX_ATTEMPTS = 10
38+
KICKSTART_MONITOR_TIMEOUT = 30 * time.Minute
39+
SERIAL_FILE_CHECK_INTERVAL = 5 * time.Second
40+
KICKSTART_SERIAL_FILE_PREFIX = "/tmp/kickstart-serial"
4241
)
4342

4443
type SKickstartSerialMonitor struct {
45-
serverId string
46-
logFilePath string
47-
serialDevice string
48-
49-
scanner *bufio.Scanner
50-
file *os.File
44+
serverId string
45+
serialFilePath string
5146

5247
ctx context.Context
5348
cancel context.CancelFunc
5449
}
5550

5651
// NewKickstartSerialMonitor creates a new kickstart serial monitor
57-
func NewKickstartSerialMonitor(serverId, logFilePath string) *SKickstartSerialMonitor {
52+
func NewKickstartSerialMonitor(serverId string) *SKickstartSerialMonitor {
5853
ctx, cancel := context.WithTimeout(context.Background(), KICKSTART_MONITOR_TIMEOUT)
54+
serialFilePath := fmt.Sprintf("%s-%s.log", KICKSTART_SERIAL_FILE_PREFIX, serverId)
5955

6056
return &SKickstartSerialMonitor{
61-
serverId: serverId,
62-
logFilePath: logFilePath,
63-
ctx: ctx,
64-
cancel: cancel,
57+
serverId: serverId,
58+
serialFilePath: serialFilePath,
59+
ctx: ctx,
60+
cancel: cancel,
6561
}
6662
}
6763

68-
// waitForSerialDevice waits for the serial device to become available
69-
func (m *SKickstartSerialMonitor) waitForSerialDevice() (string, error) {
70-
pattern := regexp.MustCompile(`char device redirected to (/dev/pts/\d+) \(label charserial0\)`)
64+
func (m *SKickstartSerialMonitor) GetSerialFilePath() string {
65+
return m.serialFilePath
66+
}
7167

72-
for attempts := 0; attempts < SERIAL_DEVICE_CHECK_MAX_ATTEMPTS; attempts++ {
73-
select {
74-
case <-m.ctx.Done():
75-
return "", errors.Errorf("context cancelled while waiting for serial device")
76-
default:
68+
func (m *SKickstartSerialMonitor) ensureSerialFile() error {
69+
if _, err := os.Stat(m.serialFilePath); os.IsNotExist(err) {
70+
file, err := os.Create(m.serialFilePath)
71+
if err != nil {
72+
return errors.Wrapf(err, "create serial file %s", m.serialFilePath)
7773
}
74+
file.Close()
75+
log.Infof("Created kickstart serial file %s for server %s", m.serialFilePath, m.serverId)
76+
}
77+
return nil
78+
}
7879

79-
if content, err := os.ReadFile(m.logFilePath); err == nil {
80-
if matches := pattern.FindStringSubmatch(string(content)); len(matches) >= 2 {
81-
devicePath := matches[1]
82-
if _, err := os.Stat(devicePath); err == nil {
83-
return devicePath, nil
84-
}
85-
}
80+
func (m *SKickstartSerialMonitor) monitorSerialFile() {
81+
defer func() {
82+
if r := recover(); r != nil {
83+
log.Errorf("KickstartSerialMonitor monitor %v %s", r, debug.Stack())
8684
}
85+
}()
8786

88-
time.Sleep(SERIAL_DEVICE_CHECK_INTERVAL)
89-
}
87+
var lastSize int64 = 0
9088

91-
return "", errors.Errorf("serial device not available after %d attempts", SERIAL_DEVICE_CHECK_MAX_ATTEMPTS)
89+
ticker := time.NewTicker(SERIAL_FILE_CHECK_INTERVAL)
90+
defer ticker.Stop()
91+
92+
for {
93+
select {
94+
case <-m.ctx.Done():
95+
return
96+
case <-ticker.C:
97+
if err := m.scanSerialForStatus(&lastSize); err != nil {
98+
log.Errorf("Failed to scan serial file for status for server %s: %v", m.serverId, err)
99+
}
100+
}
101+
}
92102
}
93103

94-
// connect establishes connection to the serial device
95-
func (m *SKickstartSerialMonitor) connect() error {
96-
devicePath, err := m.waitForSerialDevice()
104+
// scanSerialForStatus scans the serial file for a kickstart status update.
105+
// It reads new content since the last check, parses it for status keywords,
106+
// and triggers status updates and cleanup when a final status is detected.
107+
func (m *SKickstartSerialMonitor) scanSerialForStatus(lastSize *int64) error {
108+
fileInfo, err := os.Stat(m.serialFilePath)
109+
if os.IsNotExist(err) {
110+
return nil
111+
}
97112
if err != nil {
98113
return err
99114
}
100115

101-
file, err := os.Open(devicePath)
102-
if err != nil {
103-
return errors.Wrapf(err, "open serial device %s", devicePath)
116+
currentSize := fileInfo.Size()
117+
if currentSize <= *lastSize {
118+
return nil
104119
}
105120

106-
m.file = file
107-
m.serialDevice = devicePath
108-
m.scanner = bufio.NewScanner(file)
109-
110-
log.Infof("Kickstart monitor connected to serial device %s for server %s", devicePath, m.serverId)
111-
return nil
112-
}
121+
// Read new content
122+
file, err := os.Open(m.serialFilePath)
123+
if err != nil {
124+
return err
125+
}
126+
defer file.Close()
113127

114-
// read reads messages from the serial device
115-
func (m *SKickstartSerialMonitor) read() {
116-
defer func() {
117-
if r := recover(); r != nil {
118-
log.Errorf("KickstartSerialMonitor read %v %s", r, debug.Stack())
119-
}
120-
}()
128+
_, err = file.Seek(*lastSize, 0)
129+
if err != nil {
130+
return err
131+
}
121132

122-
// process messages by line
123-
scanner := m.scanner
133+
scanner := bufio.NewScanner(file)
124134
for scanner.Scan() {
125135
line := strings.TrimSpace(scanner.Text())
126136
if len(line) == 0 {
@@ -151,29 +161,29 @@ func (m *SKickstartSerialMonitor) read() {
151161
} else {
152162
log.Infof("Kickstart status for server %s updated to %s", m.serverId, status)
153163
m.Close()
164+
return nil
154165
}
155166
}
156167
}
157168

158-
if err := scanner.Err(); err != nil {
159-
log.Debugf("Kickstart serial monitor disconnected %s: %s", m.serverId, err)
160-
}
169+
*lastSize = currentSize
170+
return scanner.Err()
161171
}
162172

163-
// Close closes the serial monitor connection
164173
func (m *SKickstartSerialMonitor) Close() error {
165174
if m.cancel != nil {
166175
m.cancel()
167176
}
168177

169-
if m.file != nil {
170-
err := m.file.Close()
171-
m.file = nil
172-
m.scanner = nil
173-
log.Infof("Kickstart monitor closed for server %s", m.serverId)
174-
return err
178+
if m.serialFilePath != "" {
179+
if err := os.Remove(m.serialFilePath); err != nil && !os.IsNotExist(err) {
180+
log.Warningf("Failed to remove serial file %s: %v", m.serialFilePath, err)
181+
} else {
182+
log.Debugf("Removed serial file %s", m.serialFilePath)
183+
}
175184
}
176185

186+
log.Infof("Kickstart monitor closed for server %s", m.serverId)
177187
return nil
178188
}
179189

@@ -199,13 +209,13 @@ func (m *SKickstartSerialMonitor) updateKickstartStatus(status string) error {
199209

200210
// Start starts the kickstart serial monitor
201211
func (m *SKickstartSerialMonitor) Start() error {
202-
log.Infof("Starting kickstart monitor for server %s", m.serverId)
212+
log.Infof("Starting kickstart monitor for server %s, serial file: %s", m.serverId, m.serialFilePath)
203213

204-
if err := m.connect(); err != nil {
205-
return errors.Wrap(err, "connect to serial device")
214+
if err := m.ensureSerialFile(); err != nil {
215+
return errors.Wrap(err, "ensure serial file")
206216
}
207217

208-
go m.read()
218+
go m.monitorSerialFile()
209219

210220
// Setup timeout handler
211221
go func() {

pkg/hostman/guestman/qemu-kvm.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3708,11 +3708,11 @@ func (s *SKVMGuestInstance) startKickstartMonitorIfNeeded() {
37083708
return
37093709
}
37103710

3711-
log.Infof("Starting kickstart monitor for server %s", s.Id)
3712-
3713-
s.kickstartMonitor = NewKickstartSerialMonitor(s.Id, s.LogFilePath())
3714-
if err := s.kickstartMonitor.Start(); err != nil {
3715-
log.Errorf("Failed to start kickstart monitor for server %s: %s", s.Id, err)
3711+
if s.kickstartMonitor != nil {
3712+
log.Infof("Starting kickstart monitor for server %s", s.Id)
3713+
if err := s.kickstartMonitor.Start(); err != nil {
3714+
log.Errorf("Failed to start kickstart monitor for server %s: %s", s.Id, err)
3715+
}
37163716
}
37173717
}
37183718

pkg/hostman/guestman/qemu-kvmhelper.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -703,14 +703,21 @@ func (s *SKVMGuestInstance) handleKickstartMount(input *qemu.GenerateStartOption
703703

704704
kernelArgs := s.generateKickstartKernelArgs(kickstartConfig)
705705

706+
// Create kickstart serial monitor for status monitoring
707+
kickstartMonitor := NewKickstartSerialMonitor(s.Id)
708+
serialFilePath := kickstartMonitor.GetSerialFilePath()
709+
706710
input.KickstartBoot = &qemu.KickstartBootInfo{
707-
Config: kickstartConfig,
708-
MountPath: mountPath,
709-
KernelPath: kernelPath,
710-
InitrdPath: initrdPath,
711-
KernelArgs: kernelArgs,
711+
Config: kickstartConfig,
712+
MountPath: mountPath,
713+
KernelPath: kernelPath,
714+
InitrdPath: initrdPath,
715+
KernelArgs: kernelArgs,
716+
SerialFilePath: serialFilePath,
712717
}
713718

719+
s.kickstartMonitor = kickstartMonitor
720+
714721
log.Infof("Kickstart boot configured for guest %s: kernel=%s, initrd=%s, args=%s",
715722
s.GetName(), kernelPath, initrdPath, kernelArgs)
716723

pkg/hostman/guestman/qemu/generate.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,25 @@ func generateISASerialOptions(isaSerial *desc.SGuestIsaSerial) []string {
643643
return opts
644644
}
645645

646+
func generateKickstartSerialOptions(kickstartBoot *KickstartBootInfo) []string {
647+
if kickstartBoot == nil || kickstartBoot.SerialFilePath == "" {
648+
return nil
649+
}
650+
651+
opts := make([]string, 0)
652+
chardevId := "kickstart_serial"
653+
654+
// Create chardev with file backend
655+
chardevOpt := fmt.Sprintf("-chardev file,path=%s,id=%s", kickstartBoot.SerialFilePath, chardevId)
656+
opts = append(opts, chardevOpt)
657+
658+
// Create ISA serial device
659+
serialOpt := fmt.Sprintf("-device isa-serial,chardev=%s,id=kickstart_serial_device", chardevId)
660+
opts = append(opts, serialOpt)
661+
662+
return opts
663+
}
664+
646665
func generatePvpanicDeviceOption(pvpanic *desc.SGuestPvpanic) string {
647666
return fmt.Sprintf("-device pvpanic,id=%s,ioport=0x%x", pvpanic.Id, pvpanic.Ioport)
648667
}
@@ -703,11 +722,12 @@ type GenerateStartOptionsInput struct {
703722
KickstartBoot *KickstartBootInfo
704723
}
705724
type KickstartBootInfo struct {
706-
Config *api.KickstartConfig
707-
MountPath string
708-
KernelPath string
709-
InitrdPath string
710-
KernelArgs string
725+
Config *api.KickstartConfig
726+
MountPath string
727+
KernelPath string
728+
InitrdPath string
729+
KernelArgs string
730+
SerialFilePath string
711731
}
712732

713733
func (input *GenerateStartOptionsInput) HasBootIndex() bool {
@@ -905,6 +925,11 @@ func GenerateStartOptions(
905925
opts = append(opts, generateISASerialOptions(input.GuestDesc.IsaSerial)...)
906926
}
907927

928+
// kickstart serial device
929+
if input.KickstartBoot != nil {
930+
opts = append(opts, generateKickstartSerialOptions(input.KickstartBoot)...)
931+
}
932+
908933
// migrate options
909934
opts = append(opts, getMigrateOptions(drvOpt, input)...)
910935

0 commit comments

Comments
 (0)