diff --git a/internal/oci/uvm_test.go b/internal/oci/uvm_test.go index 70bb54d669..6b82c94de5 100644 --- a/internal/oci/uvm_test.go +++ b/internal/oci/uvm_test.go @@ -93,8 +93,8 @@ func Test_SpecToUVMCreateOptions_Default_LCOW(t *testing.T) { dopts := uvm.NewDefaultOptionsLCOW(t.Name(), "") // output handler equality is always false, so set to nil - lopts.OutputHandler = nil - dopts.OutputHandler = nil + lopts.OutputHandlerCreator = nil + dopts.OutputHandlerCreator = nil if !cmp.Equal(*lopts, *dopts) { t.Fatalf("should not have updated create options from default when no annotation are provided:\n%s", cmp.Diff(lopts, dopts)) diff --git a/internal/tools/uvmboot/lcow.go b/internal/tools/uvmboot/lcow.go index 6725c56648..9b19636350 100644 --- a/internal/tools/uvmboot/lcow.go +++ b/internal/tools/uvmboot/lcow.go @@ -227,9 +227,11 @@ func createLCOWOptions(_ context.Context, c *cli.Context, id string) (*uvm.Optio if c.IsSet(outputHandlingArgName) { switch strings.ToLower(c.String(outputHandlingArgName)) { case "stdout": - options.OutputHandler = uvm.OutputHandler(func(r io.Reader) { - _, _ = io.Copy(os.Stdout, r) - }) + options.OutputHandlerCreator = func(*uvm.Options) uvm.OutputHandler { + return func(r io.Reader) { + _, _ = io.Copy(os.Stdout, r) + } + } default: return nil, unrecognizedError(c.String(outputHandlingArgName), outputHandlingArgName) } diff --git a/internal/uvm/create_lcow.go b/internal/uvm/create_lcow.go index 3b09d6829e..37c1ebb0ae 100644 --- a/internal/uvm/create_lcow.go +++ b/internal/uvm/create_lcow.go @@ -63,9 +63,6 @@ const ( linuxLogVsockPort = 109 ) -// OutputHandler is used to process the output from the program run in the UVM. -type OutputHandler func(io.Reader) - const ( // InitrdFile is the default file name for an initrd.img used to boot LCOW. InitrdFile = "initrd.img" @@ -100,27 +97,27 @@ type OptionsLCOW struct { *Options *ConfidentialOptions - BootFilesPath string // Folder in which kernel and root file system reside. Defaults to \Program Files\Linux Containers - KernelFile string // Filename under `BootFilesPath` for the kernel. Defaults to `kernel` - KernelDirect bool // Skip UEFI and boot directly to `kernel` - RootFSFile string // Filename under `BootFilesPath` for the UVMs root file system. Defaults to `InitrdFile` - KernelBootOptions string // Additional boot options for the kernel - EnableGraphicsConsole bool // If true, enable a graphics console for the utility VM - ConsolePipe string // The named pipe path to use for the serial console. eg \\.\pipe\vmpipe - UseGuestConnection bool // Whether the HCS should connect to the UVM's GCS. Defaults to true - ExecCommandLine string // The command line to exec from init. Defaults to GCS - ForwardStdout bool // Whether stdout will be forwarded from the executed program. Defaults to false - ForwardStderr bool // Whether stderr will be forwarded from the executed program. Defaults to true - OutputHandler OutputHandler `json:"-"` // Controls how output received over HVSocket from the UVM is handled. Defaults to parsing output as logrus messages - VPMemDeviceCount uint32 // Number of VPMem devices. Defaults to `DefaultVPMEMCount`. Limit at 128. If booting UVM from VHD, device 0 is taken. - VPMemSizeBytes uint64 // Size of the VPMem devices. Defaults to `DefaultVPMemSizeBytes`. - VPMemNoMultiMapping bool // Disables LCOW layer multi mapping - PreferredRootFSType PreferredRootFSType // If `KernelFile` is `InitrdFile` use `PreferredRootFSTypeInitRd`. If `KernelFile` is `VhdFile` use `PreferredRootFSTypeVHD` - EnableColdDiscardHint bool // Whether the HCS should use cold discard hints. Defaults to false - VPCIEnabled bool // Whether the kernel should enable pci - EnableScratchEncryption bool // Whether the scratch should be encrypted - DisableTimeSyncService bool // Disables the time synchronization service - HclEnabled *bool // Whether to enable the host compatibility layer + BootFilesPath string // Folder in which kernel and root file system reside. Defaults to \Program Files\Linux Containers + KernelFile string // Filename under `BootFilesPath` for the kernel. Defaults to `kernel` + KernelDirect bool // Skip UEFI and boot directly to `kernel` + RootFSFile string // Filename under `BootFilesPath` for the UVMs root file system. Defaults to `InitrdFile` + KernelBootOptions string // Additional boot options for the kernel + EnableGraphicsConsole bool // If true, enable a graphics console for the utility VM + ConsolePipe string // The named pipe path to use for the serial console. eg \\.\pipe\vmpipe + UseGuestConnection bool // Whether the HCS should connect to the UVM's GCS. Defaults to true + ExecCommandLine string // The command line to exec from init. Defaults to GCS + ForwardStdout bool // Whether stdout will be forwarded from the executed program. Defaults to false + ForwardStderr bool // Whether stderr will be forwarded from the executed program. Defaults to true + OutputHandlerCreator OutputHandlerCreator `json:"-"` // Creates an [OutputHandler] that controls how output received over HVSocket from the UVM is handled. Defaults to parsing output as logrus messages + VPMemDeviceCount uint32 // Number of VPMem devices. Defaults to `DefaultVPMEMCount`. Limit at 128. If booting UVM from VHD, device 0 is taken. + VPMemSizeBytes uint64 // Size of the VPMem devices. Defaults to `DefaultVPMemSizeBytes`. + VPMemNoMultiMapping bool // Disables LCOW layer multi mapping + PreferredRootFSType PreferredRootFSType // If `KernelFile` is `InitrdFile` use `PreferredRootFSTypeInitRd`. If `KernelFile` is `VhdFile` use `PreferredRootFSTypeVHD` + EnableColdDiscardHint bool // Whether the HCS should use cold discard hints. Defaults to false + VPCIEnabled bool // Whether the kernel should enable pci + EnableScratchEncryption bool // Whether the scratch should be encrypted + DisableTimeSyncService bool // Disables the time synchronization service + HclEnabled *bool // Whether to enable the host compatibility layer } // defaultLCOWOSBootFilesPath returns the default path used to locate the LCOW @@ -158,7 +155,7 @@ func NewDefaultOptionsLCOW(id, owner string) *OptionsLCOW { ExecCommandLine: fmt.Sprintf("/bin/gcs -v4 -log-format json -loglevel %s", logrus.StandardLogger().Level.String()), ForwardStdout: false, ForwardStderr: true, - OutputHandler: parseLogrus(id), + OutputHandlerCreator: parseLogrus, VPMemDeviceCount: DefaultVPMEMCount, VPMemSizeBytes: DefaultVPMemSizeBytes, VPMemNoMultiMapping: osversion.Get().Build < osversion.V19H1, @@ -511,7 +508,7 @@ Example JSON document produced once the hcsschema.ComputeSytem returned by makeL } */ -// Make the ComputeSystem document object that will be serialised to json to be presented to the HCS api. +// Make the ComputeSystem document object that will be serialized to json to be presented to the HCS api. func makeLCOWDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_ *hcsschema.ComputeSystem, err error) { logrus.Tracef("makeLCOWDoc %v\n", opts) @@ -764,9 +761,9 @@ func CreateLCOW(ctx context.Context, opts *OptionsLCOW) (_ *UtilityVM, err error span.AddAttributes(trace.StringAttribute(logfields.UVMID, opts.ID)) log.G(ctx).WithField("options", fmt.Sprintf("%+v", opts)).Debug("uvm::CreateLCOW options") - // We dont serialize OutputHandler so if it is missing we need to put it back to the default. - if opts.OutputHandler == nil { - opts.OutputHandler = parseLogrus(opts.ID) + // We don't serialize OutputHandlerCreator so if it is missing we need to put it back to the default. + if opts.OutputHandlerCreator == nil { + opts.OutputHandlerCreator = parseLogrus } uvm := &UtilityVM{ @@ -829,7 +826,7 @@ func CreateLCOW(ctx context.Context, opts *OptionsLCOW) (_ *UtilityVM, err error // Create a socket that the executed program can send to. This is usually // used by GCS to send log data. if opts.ForwardStdout || opts.ForwardStderr { - uvm.outputHandler = opts.OutputHandler + uvm.outputHandler = opts.OutputHandlerCreator(opts.Options) uvm.outputProcessingDone = make(chan struct{}) uvm.outputListener, err = uvm.listenVsock(linuxLogVsockPort) if err != nil { diff --git a/internal/uvm/start.go b/internal/uvm/start.go index 53f48bb1a6..79716ab80c 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -80,7 +80,11 @@ func isDisconnectError(err error) bool { return hcs.IsAny(err, windows.WSAECONNABORTED, windows.WSAECONNRESET) } -func parseLogrus(vmid string) func(r io.Reader) { +func parseLogrus(o *Options) OutputHandler { + vmid := "" + if o != nil { + vmid = o.ID + } return func(r io.Reader) { j := json.NewDecoder(r) e := log.L.Dup() @@ -163,6 +167,11 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { // initalizing the channel and waiting on it during acceptAndClose uvm.exitCh = make(chan struct{}) + e := log.G(ctx).WithField(logfields.UVMID, uvm.id) + + // log errors in the the wait groups, since if multiple go routines return an error, + // theres no guarantee on which will be returned. + // Prepare to provide entropy to the init process in the background. This // must be done in a goroutine since, when using the internal bridge, the // call to Start() will block until the GCS launches, and this cannot occur @@ -172,12 +181,14 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { conn, err := uvm.acceptAndClose(gctx, uvm.entropyListener) uvm.entropyListener = nil if err != nil { - return fmt.Errorf("failed to connect to entropy socket: %s", err) + e.WithError(err).Error("failed to connect to entropy socket") + return fmt.Errorf("failed to connect to entropy socket: %w", err) } defer conn.Close() _, err = io.CopyN(conn, rand.Reader, entropyBytes) if err != nil { - return fmt.Errorf("failed to write entropy: %s", err) + e.WithError(err).Error("failed to write entropy") + return fmt.Errorf("failed to write entropy: %w", err) } return nil }) @@ -188,12 +199,15 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { conn, err := uvm.acceptAndClose(gctx, uvm.outputListener) uvm.outputListener = nil if err != nil { + e.WithError(err).Error("failed to connect to log socket") close(uvm.outputProcessingDone) - return fmt.Errorf("failed to connect to log socket: %s", err) + return fmt.Errorf("failed to connect to log socket: %w", err) } go func() { + e.Trace("uvm output handler starting") uvm.outputHandler(conn) close(uvm.outputProcessingDone) + e.Debug("uvm output handler finished") }() return nil }) @@ -257,7 +271,7 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { // Start the GCS protocol. gcc := &gcs.GuestConnectionConfig{ Conn: conn, - Log: log.G(ctx).WithField(logfields.UVMID, uvm.id), + Log: e, IoListen: gcs.HvsockIoListen(uvm.runtimeID), InitGuestState: initGuestState, } diff --git a/internal/uvm/types.go b/internal/uvm/types.go index 392e9b55d9..4b99c15843 100644 --- a/internal/uvm/types.go +++ b/internal/uvm/types.go @@ -3,6 +3,7 @@ package uvm import ( + "io" "net" "sync" @@ -146,3 +147,8 @@ type UtilityVM struct { func (uvm *UtilityVM) ScratchEncryptionEnabled() bool { return uvm.encryptScratch } + +// OutputHandler is used to process the output from the program run in the UVM. +type OutputHandler func(io.Reader) + +type OutputHandlerCreator func(*Options) OutputHandler