Skip to content

Commit

Permalink
Add OutputHandlerCreator type for uVMs (microsoft#1875)
Browse files Browse the repository at this point in the history
Currently, `NewDefaultOptionsLCOW` creates a logrus output handler using
the provided uVM ID, but if the `ID` field is changed, the `parseLogrus`
`OutputHandler` still uses the old ID.

Change `OptionsLCOW` to take `OutputHandlerCreator`, which is a
`func(*Options) OutputHandler`, so creating the output handler is
delayed until LCOW creation, and uses the latest uVM ID specified.

Signed-off-by: Hamza El-Saawy <[email protected]>
  • Loading branch information
helsaawy authored and ambarve committed Oct 17, 2023
1 parent ee73d47 commit 379efb1
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 40 deletions.
4 changes: 2 additions & 2 deletions internal/oci/uvm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 5 additions & 3 deletions internal/tools/uvmboot/lcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
57 changes: 27 additions & 30 deletions internal/uvm/create_lcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 19 additions & 5 deletions internal/uvm/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
})
Expand All @@ -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
})
Expand Down Expand Up @@ -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,
}
Expand Down
6 changes: 6 additions & 0 deletions internal/uvm/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package uvm

import (
"io"
"net"
"sync"

Expand Down Expand Up @@ -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

0 comments on commit 379efb1

Please sign in to comment.