Skip to content

Commit 7f4f8e7

Browse files
committed
Mirror console logs to file
Currently, the only way to observe boot logs is by attaching to the serial console manually. This often misses very early boot logs which cannot be recovered. This commit adds a log config to the serial console to ensure its output is mirrored into a file so that it can be inspected even when the early boot phase was missed. Resolves: #737 Signed-off-by: Maximilian Moehl <maximilian@moehl.eu>
1 parent 5142657 commit 7f4f8e7

4 files changed

Lines changed: 57 additions & 1 deletion

File tree

docs/concepts/console.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
# Console
1+
# Console
2+
3+
Every machine is equipped with a serial console which allows interactive access to the guest, e.g. via the
4+
`Exec` endpoint of the IRI `MachineRuntime` service.
5+
6+
## Console log
7+
8+
The console output of a machine is additionally mirrored to a log file on the hypervisor. This is an always-on
9+
feature and uses the libvirt [`<log>`](https://libvirt.org/formatdomain.html#element-log) sub-element of the
10+
serial chardev, so the interactive console stays usable while all traffic is persisted.
11+
12+
The log file is stored per machine at:
13+
14+
```text
15+
<libvirt-provider-dir>/machines/<machine-uid>/logs/console.log
16+
```
17+
18+
It is written with `append="on"`, i.e. the content is preserved across domain restarts. This allows operators
19+
to inspect early boot logs (firmware, bootloader, kernel) of a machine, which is especially useful when a
20+
machine fails to boot.
21+
22+
The console log is removed together with the machine directory when the machine is deleted.

internal/controllers/machine_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,13 @@ func (r *MachineReconciler) domainFor(
689689
Target: &libvirtxml.DomainSerialTarget{
690690
Type: serialTargetType,
691691
},
692+
// Always mirror the serial console output to a host-side log file,
693+
// so early boot logs can be inspected for troubleshooting. The
694+
// interactive pty console stays usable alongside the log.
695+
Log: &libvirtxml.DomainChardevLog{
696+
File: r.host.MachineConsoleLogFile(machine.ID),
697+
Append: "on",
698+
},
692699
},
693700
},
694701
Consoles: []libvirtxml.DomainConsole{

internal/controllers/machine_controller_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
libvirtutils "github.com/ironcore-dev/libvirt-provider/internal/libvirt/utils"
1212
. "github.com/onsi/ginkgo/v2"
1313
. "github.com/onsi/gomega"
14+
"libvirt.org/go/libvirtxml"
1415
)
1516

1617
var _ = Describe("MachineController", func() {
@@ -56,6 +57,17 @@ var _ = Describe("MachineController", func() {
5657
g.Expect(m.Status.State).To(Equal(api.MachineStateRunning))
5758
}).Should(Succeed())
5859

60+
By("ensuring the console output is mirrored to a log file")
61+
domainXML := &libvirtxml.Domain{}
62+
Expect(domainXML.Unmarshal(domainXMLData)).To(Succeed())
63+
Expect(domainXML.Devices).NotTo(BeNil())
64+
Expect(domainXML.Devices.Serials).To(HaveLen(1))
65+
consoleLog := domainXML.Devices.Serials[0].Log
66+
Expect(consoleLog).NotTo(BeNil())
67+
Expect(consoleLog.File).To(Equal(providerHost.MachineConsoleLogFile(machine.ID)))
68+
Expect(consoleLog.Append).To(Equal("on"))
69+
Expect(providerHost.MachineLogsDir(machine.ID)).To(BeADirectory())
70+
Eventually(providerHost.MachineConsoleLogFile(machine.ID)).Should(BeAnExistingFile())
5971
})
6072

6173
It("should handle machine with boot image and network interface", func(ctx SpecContext) {

internal/host/host.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const (
2525
DefaultMachineRootFSFile = "rootfs"
2626
DefaultMachinePluginsDir = "plugins"
2727
DefaultMachineNetworkInterfacesDir = "networkinterfaces"
28+
DefaultMachineLogsDir = "logs"
29+
DefaultMachineConsoleLogFile = "console.log"
2830
)
2931

3032
type Paths interface {
@@ -51,6 +53,9 @@ type Paths interface {
5153
MachineNetworkInterfacesDir(machineUID string) string
5254
MachineNetworkInterfaceDir(machineUID string, networkInterfaceName string) string
5355

56+
MachineLogsDir(machineUID string) string
57+
MachineConsoleLogFile(machineUID string) string
58+
5459
MachineIgnitionsDir(machineUID string) string
5560
MachineIgnitionFile(machineUID string) string
5661
}
@@ -127,6 +132,14 @@ func (p *paths) MachineNetworkInterfaceDir(machineUID string, networkInterfaceNa
127132
return filepath.Join(p.MachineNetworkInterfacesDir(machineUID), networkInterfaceName)
128133
}
129134

135+
func (p *paths) MachineLogsDir(machineUID string) string {
136+
return filepath.Join(p.MachineDir(machineUID), DefaultMachineLogsDir)
137+
}
138+
139+
func (p *paths) MachineConsoleLogFile(machineUID string) string {
140+
return filepath.Join(p.MachineLogsDir(machineUID), DefaultMachineConsoleLogFile)
141+
}
142+
130143
func (p *paths) MachineIgnitionsDir(machineUID string) string {
131144
return filepath.Join(p.MachineDir(machineUID), DefaultMachineIgnitionsDir)
132145
}
@@ -225,5 +238,8 @@ func MakeMachineDirs(paths Paths, machineUID string) error {
225238
if err := os.MkdirAll(paths.MachineNetworkInterfacesDir(machineUID), os.ModePerm); err != nil {
226239
return fmt.Errorf("error creating machine network interfaces directory: %w", err)
227240
}
241+
if err := os.MkdirAll(paths.MachineLogsDir(machineUID), os.ModePerm); err != nil {
242+
return fmt.Errorf("error creating machine logs directory: %w", err)
243+
}
228244
return nil
229245
}

0 commit comments

Comments
 (0)