From 76b3fbe49a46590eec8a6a5f1168dc7cfc987b8e Mon Sep 17 00:00:00 2001 From: Erik Sipsma Date: Fri, 15 Nov 2019 00:03:44 +0000 Subject: [PATCH] Support rate-limiters and read-only for drive mounts. Signed-off-by: Erik Sipsma --- proto/types.pb.go | 137 +++++++----- proto/types.proto | 10 +- runtime/drive_handler.go | 310 ++++++++++++++++++-------- runtime/drive_handler_test.go | 405 ++++++++++++++++++---------------- runtime/helpers.go | 4 + runtime/jailer.go | 9 +- runtime/noop_jailer.go | 4 +- runtime/runc_jailer.go | 16 +- runtime/service.go | 86 +++----- runtime/service_integ_test.go | 69 +++++- runtime/service_test.go | 9 +- 11 files changed, 640 insertions(+), 419 deletions(-) diff --git a/proto/types.pb.go b/proto/types.pb.go index 9b734809f..80991a8e4 100644 --- a/proto/types.pb.go +++ b/proto/types.pb.go @@ -583,7 +583,15 @@ type FirecrackerDriveMount struct { FilesystemType string `protobuf:"bytes,3,opt,name=FilesystemType,json=filesystemType,proto3" json:"FilesystemType,omitempty"` // (Optional) Options are fstab-style options that the mount will be performed // within the VM (i.e. ["rw", "noatime"]). Defaults to none if not specified. - Options []string `protobuf:"bytes,4,rep,name=Options,json=options,proto3" json:"Options,omitempty"` + Options []string `protobuf:"bytes,4,rep,name=Options,json=options,proto3" json:"Options,omitempty"` + // (Optional) RateLimiter configuration that will be applied to the + // backing-drive for the VM's rootfs + RateLimiter *FirecrackerRateLimiter `protobuf:"bytes,5,opt,name=RateLimiter,json=rateLimiter,proto3" json:"RateLimiter,omitempty"` + // (Optional) If set to true, IsWritable results in the backing file for the + // drive being opened as read-write by the Firecracker VMM on the host, allowing + // writes to the image from within the guest. Defaults to false, in which case + // the block device in the VM will be read-only. + IsWritable bool `protobuf:"varint,6,opt,name=IsWritable,json=isWritable,proto3" json:"IsWritable,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -641,6 +649,20 @@ func (m *FirecrackerDriveMount) GetOptions() []string { return nil } +func (m *FirecrackerDriveMount) GetRateLimiter() *FirecrackerRateLimiter { + if m != nil { + return m.RateLimiter + } + return nil +} + +func (m *FirecrackerDriveMount) GetIsWritable() bool { + if m != nil { + return m.IsWritable + } + return false +} + // Message to specify an IO rate limiter with bytes/s and ops/s limits type FirecrackerRateLimiter struct { Bandwidth *FirecrackerTokenBucket `protobuf:"bytes,1,opt,name=Bandwidth,json=bandwidth,proto3" json:"Bandwidth,omitempty"` @@ -760,60 +782,61 @@ func init() { func init() { proto.RegisterFile("types.proto", fileDescriptor_d938547f84707355) } var fileDescriptor_d938547f84707355 = []byte{ - // 868 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x95, 0xdd, 0x6e, 0x23, 0x35, - 0x14, 0xc7, 0x95, 0x4e, 0x93, 0xcc, 0x9c, 0x49, 0x4b, 0xb1, 0xca, 0x12, 0xba, 0x08, 0xa2, 0x11, - 0x42, 0x41, 0x5a, 0xa5, 0xa8, 0x08, 0x24, 0x2e, 0xd0, 0x2a, 0x1f, 0xbb, 0x6c, 0x80, 0xb4, 0x91, - 0x53, 0x8a, 0xc4, 0x9d, 0x33, 0x71, 0x12, 0x93, 0x8c, 0x3d, 0xb2, 0x3d, 0xc9, 0x66, 0x1f, 0x01, - 0xc1, 0x33, 0x70, 0xcd, 0x05, 0x8f, 0xc0, 0xbb, 0x21, 0x7b, 0x3c, 0xc9, 0xb4, 0xbb, 0xf4, 0x2a, - 0x3a, 0xbf, 0xff, 0x39, 0x73, 0xbe, 0x6c, 0x07, 0x42, 0xbd, 0x4b, 0xa9, 0xea, 0xa4, 0x52, 0x68, - 0x71, 0xf1, 0xd1, 0x42, 0x88, 0xc5, 0x9a, 0x5e, 0x5a, 0x6b, 0x9a, 0xcd, 0x2f, 0x09, 0xdf, 0xe5, - 0x52, 0xf4, 0x6f, 0x05, 0x82, 0x17, 0xaf, 0xb5, 0x24, 0x03, 0xa2, 0x09, 0xba, 0x00, 0xff, 0x07, - 0x25, 0xf8, 0x24, 0xa5, 0x71, 0xb3, 0xd2, 0xaa, 0xb4, 0x1b, 0xd8, 0xff, 0xcd, 0xd9, 0xe8, 0x1b, - 0x08, 0x71, 0xc6, 0xe3, 0x9b, 0x54, 0x33, 0xc1, 0x55, 0xf3, 0xa8, 0x55, 0x69, 0x87, 0x57, 0xe7, - 0x9d, 0xfc, 0xd3, 0x9d, 0xe2, 0xd3, 0x9d, 0x2e, 0xdf, 0xe1, 0x50, 0x1e, 0x1c, 0xd1, 0xc7, 0x10, - 0x4c, 0xf4, 0x8c, 0xf1, 0xb1, 0x90, 0xba, 0xe9, 0xb5, 0x2a, 0xed, 0x13, 0x1c, 0xa8, 0x02, 0xa0, - 0x4f, 0x00, 0x26, 0x7a, 0x26, 0x32, 0x6d, 0xe5, 0x63, 0x2b, 0x83, 0xda, 0x13, 0xa7, 0x53, 0x29, - 0xad, 0x5e, 0xdd, 0xeb, 0x8e, 0x44, 0xff, 0x1c, 0xc1, 0xd3, 0x97, 0x4c, 0xd2, 0x58, 0x92, 0x78, - 0x45, 0xe5, 0x35, 0xd5, 0x5b, 0x21, 0x57, 0x43, 0xae, 0xa9, 0x9c, 0x93, 0x98, 0x9a, 0xec, 0xdd, - 0xf5, 0x5a, 0x6c, 0x47, 0xa3, 0xc1, 0xc4, 0xb6, 0xe4, 0xe3, 0x80, 0x14, 0x00, 0x7d, 0x07, 0x27, - 0x43, 0x8e, 0x89, 0xa6, 0x3f, 0xb1, 0x84, 0x69, 0x2a, 0x5d, 0x57, 0x1f, 0x76, 0x4a, 0x9f, 0x2c, - 0xc9, 0xf8, 0x84, 0x95, 0xbd, 0xd1, 0x73, 0x38, 0xbd, 0xc9, 0x74, 0x39, 0xde, 0x7b, 0x3c, 0xfe, - 0x54, 0xdc, 0x73, 0x47, 0x97, 0x10, 0xf4, 0xaf, 0x87, 0x7d, 0xc1, 0xe7, 0x6c, 0x61, 0x9b, 0x0f, - 0xaf, 0xde, 0xef, 0xec, 0x49, 0x26, 0x89, 0x19, 0x21, 0x0e, 0xe2, 0x82, 0xa0, 0xe7, 0xd0, 0x98, - 0x68, 0xa2, 0x59, 0xec, 0x62, 0xaa, 0x36, 0xe6, 0x69, 0x27, 0x87, 0xae, 0xfb, 0xfb, 0xd1, 0x0d, - 0x55, 0x0a, 0x88, 0x7e, 0x3f, 0x82, 0xb3, 0x87, 0x09, 0x50, 0x0b, 0x42, 0x17, 0x7a, 0x4d, 0x12, - 0x6a, 0xc7, 0x14, 0xe0, 0x90, 0x1f, 0x10, 0xfa, 0xcc, 0x0c, 0xca, 0xcd, 0xd4, 0xfa, 0x1c, 0x59, - 0x9f, 0x13, 0x56, 0x86, 0xa8, 0x09, 0xf5, 0x1e, 0xe3, 0x63, 0xa2, 0x97, 0x4d, 0xaf, 0xe5, 0xb5, - 0x03, 0x5c, 0x9f, 0xe6, 0xa6, 0x51, 0x4c, 0xca, 0x01, 0x93, 0xb6, 0xcd, 0x00, 0xd7, 0xe3, 0xdc, - 0x34, 0x47, 0xae, 0x4f, 0xe2, 0x25, 0x35, 0x52, 0xd5, 0x4a, 0x7e, 0xec, 0x6c, 0xf4, 0x0c, 0x8e, - 0xbb, 0x72, 0xa1, 0x9a, 0xb5, 0x96, 0xd7, 0x0e, 0xaf, 0x9a, 0x6f, 0x4d, 0xc6, 0x80, 0xae, 0x5c, - 0xe0, 0x63, 0x22, 0x17, 0xea, 0xe2, 0x4b, 0xa8, 0xe5, 0x36, 0x3a, 0x03, 0xef, 0x47, 0xba, 0x73, - 0x7d, 0x78, 0x2b, 0xba, 0x43, 0xe7, 0x50, 0xbd, 0x23, 0xeb, 0xac, 0xa8, 0xbb, 0xba, 0x31, 0x46, - 0xf4, 0x47, 0x05, 0x2e, 0xfe, 0x7f, 0x72, 0xe6, 0xec, 0x8d, 0x48, 0xdc, 0x9d, 0xcd, 0x24, 0x55, - 0xca, 0x7d, 0x0d, 0x92, 0x3d, 0x31, 0x63, 0x7b, 0x25, 0x94, 0x1e, 0xd0, 0x4d, 0x69, 0x24, 0xe1, - 0xf2, 0x80, 0xd0, 0x33, 0xf0, 0x87, 0x63, 0xb7, 0xaa, 0xfc, 0x68, 0x9c, 0x75, 0x0a, 0x50, 0xec, - 0xc7, 0x67, 0x0e, 0x44, 0x5b, 0x78, 0xef, 0x81, 0x68, 0x52, 0x8c, 0x25, 0x4b, 0x88, 0xdc, 0x99, - 0xa4, 0xc5, 0x66, 0xd2, 0x03, 0x32, 0x1e, 0xdf, 0x13, 0x4d, 0xb7, 0x24, 0xf7, 0xf0, 0x72, 0x8f, - 0xc5, 0x01, 0xd9, 0xed, 0x92, 0x84, 0x2a, 0x2a, 0x37, 0x54, 0xaa, 0xe6, 0xb1, 0xdd, 0x4c, 0xc8, - 0x0f, 0x28, 0xfa, 0xab, 0x02, 0x9f, 0x96, 0x4e, 0xec, 0x88, 0xc4, 0x4b, 0xc6, 0xe9, 0x5b, 0x95, - 0xf4, 0xc7, 0x3f, 0xdf, 0xd2, 0x24, 0x5d, 0x13, 0xbd, 0x3f, 0x23, 0xf1, 0x01, 0x99, 0xab, 0xf6, - 0x4a, 0xbf, 0xe0, 0x64, 0xba, 0xa6, 0x33, 0x3b, 0x0c, 0x1f, 0x07, 0xcb, 0x02, 0xd8, 0x61, 0xd2, - 0x64, 0xc2, 0xde, 0xd0, 0x11, 0x9b, 0xba, 0x77, 0x00, 0x92, 0x3d, 0x31, 0xd1, 0x77, 0x71, 0x9a, - 0xf5, 0x45, 0xc6, 0x8b, 0x77, 0x20, 0xd8, 0x14, 0x20, 0xfa, 0xbb, 0x02, 0xe7, 0xe5, 0x3b, 0x25, - 0x84, 0x1e, 0x48, 0xb6, 0xa1, 0xe6, 0xf8, 0x98, 0x1d, 0xd8, 0x33, 0x97, 0xd7, 0xe4, 0x2f, 0x9d, - 0x6d, 0xb4, 0x31, 0x91, 0x3a, 0xcb, 0xd8, 0xcc, 0x2d, 0xc7, 0x4f, 0x9d, 0x6d, 0xca, 0x19, 0xaa, - 0x5f, 0x24, 0xd3, 0xa6, 0x3a, 0x5b, 0x8e, 0x8f, 0x81, 0xed, 0x09, 0xfa, 0x16, 0xc2, 0xf2, 0xbd, - 0x3e, 0x7e, 0xfc, 0x5e, 0x87, 0xf2, 0x60, 0x44, 0x7f, 0x56, 0xe0, 0x83, 0x92, 0x9f, 0xad, 0x73, - 0x64, 0xba, 0x78, 0xb4, 0xd8, 0x27, 0x50, 0xbb, 0x1b, 0x59, 0x25, 0x2f, 0xb5, 0xb6, 0xb1, 0x16, - 0xfa, 0x1c, 0x4e, 0x5f, 0xb2, 0x35, 0x55, 0x3b, 0xa5, 0x69, 0x72, 0xbb, 0x4b, 0xa9, 0x5b, 0xf1, - 0xe9, 0xfc, 0x1e, 0x35, 0x37, 0xac, 0x78, 0x9a, 0xf3, 0x0d, 0xd7, 0x45, 0x6e, 0x46, 0x6f, 0xe0, - 0xc9, 0xbb, 0xcb, 0x46, 0x5f, 0x43, 0xd0, 0x23, 0x7c, 0xb6, 0x65, 0x33, 0x57, 0xd0, 0x83, 0x16, - 0x6f, 0xc5, 0x8a, 0xf2, 0x5e, 0x16, 0xaf, 0xa8, 0xc6, 0xc1, 0xb4, 0xf0, 0x44, 0x5f, 0x80, 0x77, - 0x93, 0xaa, 0x77, 0xbd, 0x95, 0xe5, 0x00, 0x4f, 0xa4, 0x2a, 0x7a, 0x7d, 0x2f, 0x77, 0x49, 0x46, - 0x11, 0x34, 0x6e, 0x38, 0xbd, 0x65, 0x09, 0xed, 0x65, 0x52, 0x69, 0x9b, 0xde, 0xc3, 0x0d, 0x51, - 0x62, 0x66, 0x49, 0x98, 0xce, 0xd9, 0x7a, 0x6d, 0x90, 0xcd, 0xe7, 0x61, 0x90, 0x7b, 0x92, 0xbf, - 0x1d, 0x29, 0x89, 0x99, 0xde, 0xd9, 0xa9, 0x78, 0xe6, 0xed, 0xc8, 0xed, 0x5e, 0xfd, 0xd7, 0x6a, - 0xfe, 0x9f, 0x54, 0xb3, 0x3f, 0x5f, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0x35, 0x4e, 0x86, 0xff, - 0x12, 0x07, 0x00, 0x00, + // 885 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0xdd, 0x8e, 0xe3, 0x44, + 0x13, 0x55, 0xc6, 0xf9, 0xb1, 0xcb, 0x99, 0xf9, 0xe6, 0x6b, 0x0d, 0x4b, 0x98, 0x45, 0x10, 0x59, + 0x08, 0x05, 0x69, 0x95, 0x41, 0x83, 0x40, 0xe2, 0x02, 0xad, 0xf2, 0xb3, 0xcb, 0x06, 0xc8, 0x4c, + 0xe4, 0x0c, 0x83, 0xc4, 0x5d, 0xc7, 0xe9, 0x24, 0x4d, 0xe2, 0x6e, 0xab, 0xbb, 0x9d, 0xac, 0xf7, + 0x11, 0x10, 0xef, 0xc0, 0x35, 0x17, 0x3c, 0x02, 0xef, 0xc3, 0x63, 0xa0, 0x6e, 0xb7, 0x13, 0x4f, + 0x76, 0x19, 0x71, 0x15, 0xd5, 0x39, 0x55, 0xae, 0xaa, 0x53, 0xd5, 0x15, 0xf0, 0x55, 0x96, 0x10, + 0xd9, 0x4d, 0x04, 0x57, 0xfc, 0xf2, 0x83, 0x25, 0xe7, 0xcb, 0x0d, 0xb9, 0x32, 0xd6, 0x2c, 0x5d, + 0x5c, 0x61, 0x96, 0xe5, 0x54, 0xf0, 0x57, 0x05, 0xbc, 0x17, 0xaf, 0x95, 0xc0, 0x43, 0xac, 0x30, + 0xba, 0x04, 0xf7, 0x3b, 0xc9, 0xd9, 0x34, 0x21, 0x51, 0xab, 0xd2, 0xae, 0x74, 0x9a, 0xa1, 0xfb, + 0x8b, 0xb5, 0xd1, 0x57, 0xe0, 0x87, 0x29, 0x8b, 0x6e, 0x13, 0x45, 0x39, 0x93, 0xad, 0x93, 0x76, + 0xa5, 0xe3, 0x5f, 0x5f, 0x74, 0xf3, 0x4f, 0x77, 0x8b, 0x4f, 0x77, 0x7b, 0x2c, 0x0b, 0x7d, 0x71, + 0x70, 0x44, 0x1f, 0x82, 0x37, 0x55, 0x73, 0xca, 0x26, 0x5c, 0xa8, 0x96, 0xd3, 0xae, 0x74, 0x4e, + 0x43, 0x4f, 0x16, 0x00, 0xfa, 0x08, 0x60, 0xaa, 0xe6, 0x3c, 0x55, 0x86, 0xae, 0x1a, 0x1a, 0xe4, + 0x1e, 0xb1, 0x3c, 0x11, 0xc2, 0xf0, 0xb5, 0x3d, 0x6f, 0x91, 0xe0, 0xcf, 0x13, 0x78, 0xfa, 0x92, + 0x0a, 0x12, 0x09, 0x1c, 0xad, 0x89, 0xb8, 0x21, 0x6a, 0xc7, 0xc5, 0x7a, 0xc4, 0x14, 0x11, 0x0b, + 0x1c, 0x11, 0x9d, 0xbd, 0xb7, 0xd9, 0xf0, 0xdd, 0x78, 0x3c, 0x9c, 0x9a, 0x96, 0xdc, 0xd0, 0xc3, + 0x05, 0x80, 0xbe, 0x81, 0xd3, 0x11, 0x0b, 0xb1, 0x22, 0x3f, 0xd0, 0x98, 0x2a, 0x22, 0x6c, 0x57, + 0xef, 0x77, 0x4b, 0x9f, 0x2c, 0xd1, 0xe1, 0x29, 0x2d, 0x7b, 0xa3, 0xe7, 0x70, 0x76, 0x9b, 0xaa, + 0x72, 0xbc, 0xf3, 0x78, 0xfc, 0x19, 0x7f, 0xe0, 0x8e, 0xae, 0xc0, 0x1b, 0xdc, 0x8c, 0x06, 0x9c, + 0x2d, 0xe8, 0xd2, 0x34, 0xef, 0x5f, 0xff, 0xbf, 0xbb, 0x47, 0x52, 0x81, 0xb5, 0x84, 0xa1, 0x17, + 0x15, 0x08, 0x7a, 0x0e, 0xcd, 0xa9, 0xc2, 0x8a, 0x46, 0x36, 0xa6, 0x66, 0x62, 0x9e, 0x76, 0x73, + 0xd0, 0x76, 0xff, 0x30, 0xba, 0x29, 0x4b, 0x01, 0xc1, 0xaf, 0x27, 0x70, 0x7e, 0x9c, 0x00, 0xb5, + 0xc1, 0xb7, 0xa1, 0x37, 0x38, 0x26, 0x46, 0x26, 0x2f, 0xf4, 0xd9, 0x01, 0x42, 0x9f, 0x68, 0xa1, + 0xac, 0xa6, 0xc6, 0xe7, 0xc4, 0xf8, 0x9c, 0xd2, 0x32, 0x88, 0x5a, 0xd0, 0xe8, 0x53, 0x36, 0xc1, + 0x6a, 0xd5, 0x72, 0xda, 0x4e, 0xc7, 0x0b, 0x1b, 0xb3, 0xdc, 0xd4, 0x8c, 0x4e, 0x39, 0xa4, 0xc2, + 0xb4, 0xe9, 0x85, 0x8d, 0x28, 0x37, 0xf5, 0xca, 0x0d, 0x70, 0xb4, 0x22, 0x9a, 0xaa, 0x19, 0xca, + 0x8d, 0xac, 0x8d, 0x9e, 0x41, 0xb5, 0x27, 0x96, 0xb2, 0x55, 0x6f, 0x3b, 0x1d, 0xff, 0xba, 0xf5, + 0x96, 0x32, 0x1a, 0xe8, 0x89, 0x65, 0x58, 0xc5, 0x62, 0x29, 0x2f, 0x3f, 0x87, 0x7a, 0x6e, 0xa3, + 0x73, 0x70, 0xbe, 0x27, 0x99, 0xed, 0xc3, 0x59, 0x93, 0x0c, 0x5d, 0x40, 0xed, 0x1e, 0x6f, 0xd2, + 0xa2, 0xee, 0xda, 0x56, 0x1b, 0xc1, 0x6f, 0x15, 0xb8, 0xfc, 0x77, 0xe5, 0xf4, 0xee, 0x8d, 0x71, + 0xd4, 0x9b, 0xcf, 0x05, 0x91, 0xd2, 0x7e, 0x0d, 0xe2, 0x3d, 0xa2, 0x65, 0x7b, 0xc5, 0xa5, 0x1a, + 0x92, 0x6d, 0x49, 0x12, 0x7f, 0x75, 0x80, 0xd0, 0x33, 0x70, 0x47, 0x13, 0x3b, 0xaa, 0x7c, 0x35, + 0xce, 0xbb, 0x05, 0x50, 0xcc, 0xc7, 0xa5, 0x16, 0x08, 0x76, 0xf0, 0xbf, 0x23, 0x52, 0xa7, 0x98, + 0x08, 0x1a, 0x63, 0x91, 0xe9, 0xa4, 0xc5, 0x64, 0x92, 0x03, 0xa4, 0x3d, 0xbe, 0xc5, 0x8a, 0xec, + 0x70, 0xee, 0xe1, 0xe4, 0x1e, 0xcb, 0x03, 0x64, 0xa6, 0x8b, 0x63, 0x22, 0x89, 0xd8, 0x12, 0x21, + 0x5b, 0x55, 0x33, 0x19, 0x9f, 0x1d, 0xa0, 0xe0, 0xf7, 0x0a, 0x7c, 0x5c, 0xda, 0xd8, 0x31, 0x8e, + 0x56, 0x94, 0x91, 0xb7, 0x2a, 0x19, 0x4c, 0x7e, 0xbc, 0x23, 0x71, 0xb2, 0xc1, 0x6a, 0xbf, 0x23, + 0xd1, 0x01, 0xd2, 0x4f, 0xed, 0x95, 0x7a, 0xc1, 0xf0, 0x6c, 0x43, 0xe6, 0x46, 0x0c, 0x37, 0xf4, + 0x56, 0x05, 0x60, 0xc4, 0x24, 0xf1, 0x94, 0xbe, 0x21, 0x63, 0x3a, 0xb3, 0x77, 0x00, 0xe2, 0x3d, + 0xa2, 0xa3, 0xef, 0xa3, 0x24, 0x1d, 0xf0, 0x94, 0x15, 0x77, 0xc0, 0xdb, 0x16, 0x40, 0xf0, 0x47, + 0x05, 0x2e, 0xca, 0x6f, 0x8a, 0x73, 0x35, 0x14, 0x74, 0x4b, 0xf4, 0xfa, 0xe8, 0x19, 0x98, 0x9d, + 0xcb, 0x6b, 0x72, 0x57, 0xd6, 0xd6, 0xdc, 0x04, 0x0b, 0x95, 0xa6, 0x74, 0x6e, 0x87, 0xe3, 0x26, + 0xd6, 0xd6, 0xe5, 0x8c, 0xe4, 0x4f, 0x82, 0x2a, 0x5d, 0x9d, 0x29, 0xc7, 0x0d, 0x81, 0xee, 0x11, + 0xf4, 0x35, 0xf8, 0xe5, 0x77, 0x5d, 0x7d, 0xfc, 0x5d, 0xfb, 0xe2, 0x60, 0x04, 0x7f, 0x57, 0xe0, + 0xbd, 0x92, 0x9f, 0xa9, 0x73, 0xac, 0xbb, 0x78, 0xb4, 0xd8, 0x27, 0x50, 0xbf, 0x1f, 0x1b, 0x26, + 0x2f, 0xb5, 0xbe, 0x35, 0x16, 0xfa, 0x14, 0xce, 0x5e, 0xd2, 0x0d, 0x91, 0x99, 0x54, 0x24, 0xbe, + 0xcb, 0x12, 0x62, 0x47, 0x7c, 0xb6, 0x78, 0x80, 0xea, 0x17, 0x56, 0x9c, 0xe6, 0x7c, 0xc2, 0x0d, + 0x6e, 0x0f, 0xf0, 0x51, 0x2b, 0xb5, 0xff, 0xde, 0xca, 0x91, 0x4a, 0xf5, 0x63, 0x95, 0x82, 0x37, + 0xf0, 0xe4, 0xdd, 0x9f, 0x41, 0x5f, 0x82, 0xd7, 0xc7, 0x6c, 0xbe, 0xa3, 0x73, 0xdb, 0xeb, 0x51, + 0xca, 0x3b, 0xbe, 0x26, 0xac, 0x9f, 0x46, 0x6b, 0xa2, 0x42, 0x6f, 0x56, 0x78, 0xa2, 0xcf, 0xc0, + 0xb9, 0x4d, 0xe4, 0xbb, 0xce, 0x70, 0x39, 0xc0, 0xe1, 0x89, 0x0c, 0x5e, 0x3f, 0xc8, 0x5d, 0xa2, + 0x51, 0x00, 0xcd, 0x5b, 0x46, 0xee, 0x68, 0x4c, 0xfa, 0xa9, 0x90, 0xca, 0xa4, 0x77, 0xc2, 0x26, + 0x2f, 0x61, 0xba, 0xb3, 0x90, 0x2c, 0xe8, 0x66, 0xa3, 0x21, 0x93, 0xcf, 0x09, 0x41, 0xec, 0x91, + 0xfc, 0x2c, 0x25, 0x38, 0xa2, 0x2a, 0x33, 0x82, 0x3b, 0xfa, 0x2c, 0xe5, 0x76, 0xbf, 0xf1, 0x73, + 0x2d, 0xff, 0xbb, 0xab, 0x9b, 0x9f, 0x2f, 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0xc9, 0x27, 0x3b, + 0x3e, 0x6d, 0x07, 0x00, 0x00, } diff --git a/proto/types.proto b/proto/types.proto index dc5b1d3f2..81a1755f5 100644 --- a/proto/types.proto +++ b/proto/types.proto @@ -144,7 +144,15 @@ message FirecrackerDriveMount { // within the VM (i.e. ["rw", "noatime"]). Defaults to none if not specified. repeated string Options = 4; - // TODO support for ratelimiters and read-only drives (issue #296) + // (Optional) RateLimiter configuration that will be applied to the + // backing-drive for the VM's rootfs + FirecrackerRateLimiter RateLimiter = 5; + + // (Optional) If set to true, IsWritable results in the backing file for the + // drive being opened as read-write by the Firecracker VMM on the host, allowing + // writes to the image from within the guest. Defaults to false, in which case + // the block device in the VM will be read-only. + bool IsWritable = 6; } // Message to specify an IO rate limiter with bytes/s and ops/s limits diff --git a/runtime/drive_handler.go b/runtime/drive_handler.go index f5f958e47..6ac09bf51 100644 --- a/runtime/drive_handler.go +++ b/runtime/drive_handler.go @@ -15,21 +15,25 @@ package main import ( "context" + "encoding/base32" "fmt" "os" "path/filepath" "sync" + "github.com/pkg/errors" "github.com/sirupsen/logrus" firecracker "github.com/firecracker-microvm/firecracker-go-sdk" - models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" + "github.com/firecracker-microvm/firecracker-go-sdk/client/models" "github.com/firecracker-microvm/firecracker-containerd/internal" + "github.com/firecracker-microvm/firecracker-containerd/proto" + drivemount "github.com/firecracker-microvm/firecracker-containerd/proto/service/drivemount/ttrpc" ) const ( - // fcSectorSize is the sector size of Firecracker + // fcSectorSize is the sector size of Firecracker drives fcSectorSize = 512 ) @@ -37,103 +41,203 @@ var ( // ErrDrivesExhausted occurs when there are no more drives left to use. This // can happen by calling PatchStubDrive greater than the number of drives. ErrDrivesExhausted = fmt.Errorf("There are no remaining drives to be used") - - // ErrDriveIDNil should never happen, but we safe guard against nil dereferencing - ErrDriveIDNil = fmt.Errorf("DriveID of current drive is nil") ) -// stubDriveHandler is used to manage stub drives. -type stubDriveHandler struct { - RootPath string - stubDriveIndex int64 - drives []models.Drive - logger *logrus.Entry - mutex sync.Mutex +// CreateContainerStubs will create a StubDriveHandler for managing the stub drives +// of container rootfs drives. The Firecracker drives are hardcoded to be read-write +// and have no rate limiter configuration. +func CreateContainerStubs( + machineCfg *firecracker.Config, + jail jailer, + containerCount int, + logger *logrus.Entry, +) (*StubDriveHandler, error) { + var containerStubs []*stubDrive + for i := 0; i < containerCount; i++ { + isWritable := true + var rateLimiter *proto.FirecrackerRateLimiter + stubFileName := fmt.Sprintf("ctrstub%d", i) + + stubDrive, err := newStubDrive( + filepath.Join(jail.JailPath().RootPath(), stubFileName), + jail, isWritable, rateLimiter, logger) + + if err != nil { + return nil, errors.Wrap(err, "failed to create container stub drive") + } + + machineCfg.Drives = append(machineCfg.Drives, models.Drive{ + DriveID: firecracker.String(stubDrive.driveID), + PathOnHost: firecracker.String(stubDrive.stubPath), + IsReadOnly: firecracker.Bool(!isWritable), + RateLimiter: rateLimiterFromProto(rateLimiter), + IsRootDevice: firecracker.Bool(false), + }) + containerStubs = append(containerStubs, stubDrive) + } + + return &StubDriveHandler{ + freeDrives: containerStubs, + usedDrives: make(map[string]*stubDrive), + }, nil } -// stubDrivesOpt is used to make and modify changes to the stub drives. -type stubDrivesOpt func(stubDrives []models.Drive) error +// StubDriveHandler manages a set of stub drives. It currently only supports reserving +// one of the drives from its set. +// In the future, it may be expanded to also support recycling a drive to be used again +// for a different mount. +type StubDriveHandler struct { + freeDrives []*stubDrive + // map of id -> stub drive being used by that task + usedDrives map[string]*stubDrive + mu sync.Mutex +} -func newStubDriveHandler(path string, logger *logrus.Entry, count int, opts ...stubDrivesOpt) (*stubDriveHandler, error) { - h := stubDriveHandler{ - RootPath: path, - logger: logger, +// Reserve pops a unused stub drive and returns a MountableStubDrive that can be +// mounted with the provided options as the patched drive information. +func (h *StubDriveHandler) Reserve( + id string, + hostPath string, + vmPath string, + filesystemType string, + options []string, +) (MountableStubDrive, error) { + h.mu.Lock() + defer h.mu.Unlock() + if len(h.freeDrives) == 0 { + return nil, ErrDrivesExhausted } - drives, err := h.createStubDrives(count) + + freeDrive := h.freeDrives[0] + options, err := setReadWriteOptions(options, freeDrive.driveMount.IsWritable) if err != nil { return nil, err } - for _, opt := range opts { - if err := opt(drives); err != nil { - h.logger.WithError(err).Debug("failed to apply option to stub drives") + h.freeDrives = h.freeDrives[1:] + h.usedDrives[id] = freeDrive + return freeDrive.withMountConfig( + hostPath, + vmPath, + filesystemType, + options, + ), nil +} + +// CreateDriveMountStubs creates a set of MountableStubDrives from the provided DriveMount configs. +// The RateLimiter and ReadOnly settings need to be provided up front here as they currently +// cannot be patched after the Firecracker VM starts. +func CreateDriveMountStubs( + machineCfg *firecracker.Config, + jail jailer, + driveMounts []*proto.FirecrackerDriveMount, + logger *logrus.Entry, +) ([]MountableStubDrive, error) { + containerStubs := make([]MountableStubDrive, len(driveMounts)) + for i, driveMount := range driveMounts { + isWritable := driveMount.IsWritable + rateLimiter := driveMount.RateLimiter + stubFileName := fmt.Sprintf("drivemntstub%d", i) + options, err := setReadWriteOptions(driveMount.Options, isWritable) + if err != nil { return nil, err } - } - h.drives = drives - return &h, nil -} -func (h *stubDriveHandler) createStubDrives(stubDriveCount int) ([]models.Drive, error) { - paths, err := h.stubDrivePaths(stubDriveCount) - if err != nil { - return nil, err - } + stubDrive, err := newStubDrive( + filepath.Join(jail.JailPath().RootPath(), stubFileName), + jail, isWritable, rateLimiter, logger) + if err != nil { + return nil, errors.Wrap(err, "failed to create drive mount stub drive") + } - stubDrives := make([]models.Drive, 0, stubDriveCount) - for i, path := range paths { - stubDrives = append(stubDrives, models.Drive{ - DriveID: firecracker.String(fmt.Sprintf("stub%d", i)), - IsReadOnly: firecracker.Bool(false), - PathOnHost: firecracker.String(path), + machineCfg.Drives = append(machineCfg.Drives, models.Drive{ + DriveID: firecracker.String(stubDrive.driveID), + PathOnHost: firecracker.String(stubDrive.stubPath), + IsReadOnly: firecracker.Bool(!isWritable), + RateLimiter: rateLimiterFromProto(rateLimiter), IsRootDevice: firecracker.Bool(false), }) + containerStubs[i] = stubDrive.withMountConfig( + driveMount.HostPath, + driveMount.VMPath, + driveMount.FilesystemType, + options) } - return stubDrives, nil + return containerStubs, nil } -// stubDrivePaths will create stub drives and return the paths associated with -// the stub drives. -func (h *stubDriveHandler) stubDrivePaths(count int) ([]string, error) { - paths := []string{} - for i := 0; i < count; i++ { - driveID := fmt.Sprintf("stub%d", i) - path := filepath.Join(h.RootPath, driveID) +func setReadWriteOptions(options []string, isWritable bool) ([]string, error) { + var expectedOpt string + if isWritable { + expectedOpt = "rw" + } else { + expectedOpt = "ro" + } - if err := h.createStubDrive(driveID, path); err != nil { - return nil, err + for _, opt := range options { + if opt == "ro" || opt == "rw" { + if opt != expectedOpt { + return nil, errors.Errorf("mount option %s is incompatible with IsWritable=%t", opt, isWritable) + } + return options, nil } - - paths = append(paths, path) } - return paths, nil + // if here, the neither "ro" or "rw" was specified, so explicitly set the option for the user + return append(options, expectedOpt), nil +} + +// A MountableStubDrive represents a stub drive that is ready to be patched and mounted +// once PatchAndMount is called. +type MountableStubDrive interface { + PatchAndMount( + requestCtx context.Context, + machine firecracker.MachineIface, + driveMounter drivemount.DriveMounterService, + ) error +} + +func stubPathToDriveID(stubPath string) string { + // Firecracker resource ids "can only contain alphanumeric characters and underscores", so + // do a base32 encoding to remove any invalid characters (base32 avoids invalid "-" chars + // from base64) + return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte( + filepath.Base(stubPath))) } -func (h *stubDriveHandler) createStubDrive(driveID, path string) error { - f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) +func newStubDrive( + stubPath string, + jail jailer, + isWritable bool, + rateLimiter *proto.FirecrackerRateLimiter, + logger *logrus.Entry, +) (*stubDrive, error) { + // use the stubPath as the drive ID since it needs to be unique per-stubdrive anyways + driveID := stubPathToDriveID(stubPath) + + f, err := os.OpenFile(stubPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if err != nil { - return err + return nil, err } defer func() { if err := f.Close(); err != nil { - h.logger.WithError(err).Errorf("unexpected error during %v close", f.Name()) + logger.WithError(err).Errorf("unexpected error during %v close", f.Name()) } }() stubContent, err := internal.GenerateStubContent(driveID) if err != nil { - return err + return nil, err } if _, err := f.WriteString(stubContent); err != nil { - return err + return nil, err } info, err := f.Stat() if err != nil { - return err + return nil, err } fileSize := info.Size() @@ -153,56 +257,76 @@ func (h *stubDriveHandler) createStubDrive(driveID, path string) error { // not a multiple of 512 bytes, then the remainder will not be visible to // Firecracker. So we adjust to the appropriate size based on the residual // bytes remaining. - if err := os.Truncate(path, driveSize); err != nil { - return err + if err := os.Truncate(stubPath, driveSize); err != nil { + return nil, err } - return nil -} - -// GetDrives returns the associated stub drives -func (h *stubDriveHandler) GetDrives() []models.Drive { - return h.drives -} - -// InDriveSet will iterate through all the stub drives and see if the path -// exists on any of the drives -func (h *stubDriveHandler) InDriveSet(path string) bool { - for _, d := range h.GetDrives() { - if firecracker.StringValue(d.PathOnHost) == path { - return true + for _, opt := range jail.StubDrivesOptions() { + err := opt(f) + if err != nil { + return nil, err } } - return false + return &stubDrive{ + stubPath: stubPath, + jail: jail, + driveID: driveID, + driveMount: &proto.FirecrackerDriveMount{ + IsWritable: isWritable, + RateLimiter: rateLimiter, + }, + }, nil } -// PatchStubDrive will replace the next available stub drive with the provided drive -func (h *stubDriveHandler) PatchStubDrive(ctx context.Context, client firecracker.MachineIface, pathOnHost string) (string, error) { - h.mutex.Lock() - defer h.mutex.Unlock() +type stubDrive struct { + stubPath string + jail jailer + driveID string + driveMount *proto.FirecrackerDriveMount +} - // Check to see if stubDriveIndex has increased more than the drive amount. - if h.stubDriveIndex >= int64(len(h.drives)) { - return "", ErrDrivesExhausted +func (sd stubDrive) withMountConfig( + hostPath string, + vmPath string, + filesystemType string, + options []string, +) stubDrive { + sd.driveMount = &proto.FirecrackerDriveMount{ + HostPath: hostPath, + VMPath: vmPath, + FilesystemType: filesystemType, + Options: options, + IsWritable: sd.driveMount.IsWritable, + RateLimiter: sd.driveMount.RateLimiter, } + return sd +} - d := h.drives[h.stubDriveIndex] - d.PathOnHost = &pathOnHost - - if d.DriveID == nil { - // this should never happen, but we want to ensure that we never nil - // dereference - return "", ErrDriveIDNil +func (sd stubDrive) PatchAndMount( + requestCtx context.Context, + machine firecracker.MachineIface, + driveMounter drivemount.DriveMounterService, +) error { + err := sd.jail.ExposeFileToJail(sd.driveMount.HostPath) + if err != nil { + return errors.Wrap(err, "failed to expose patched drive contents to jail") } - h.drives[h.stubDriveIndex] = d + err = machine.UpdateGuestDrive(requestCtx, sd.driveID, sd.driveMount.HostPath) + if err != nil { + return errors.Wrap(err, "failed to patch drive") + } - err := client.UpdateGuestDrive(ctx, firecracker.StringValue(d.DriveID), pathOnHost) + _, err = driveMounter.MountDrive(requestCtx, &drivemount.MountDriveRequest{ + DriveID: sd.driveID, + DestinationPath: sd.driveMount.VMPath, + FilesytemType: sd.driveMount.FilesystemType, + Options: sd.driveMount.Options, + }) if err != nil { - return "", err + return errors.Wrap(err, "failed to mount newly patched drive") } - h.stubDriveIndex++ - return firecracker.StringValue(d.DriveID), nil + return nil } diff --git a/runtime/drive_handler_test.go b/runtime/drive_handler_test.go index 2f27a14e3..6a642171e 100644 --- a/runtime/drive_handler_test.go +++ b/runtime/drive_handler_test.go @@ -15,236 +15,257 @@ package main import ( "context" - "io/ioutil" "os" "path/filepath" - "strings" - "sync" + "strconv" "testing" - "github.com/containerd/containerd/log" + "io/ioutil" - "github.com/firecracker-microvm/firecracker-go-sdk" - models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" + "github.com/containerd/containerd/log" + "github.com/firecracker-microvm/firecracker-containerd/internal/vm" + "github.com/firecracker-microvm/firecracker-containerd/proto" + drivemount "github.com/firecracker-microvm/firecracker-containerd/proto/service/drivemount/ttrpc" + firecracker "github.com/firecracker-microvm/firecracker-go-sdk" ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" "github.com/firecracker-microvm/firecracker-go-sdk/fctesting" + "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestStubDriveHandler(t *testing.T) { - tempPath, err := ioutil.TempDir("", "TestStubDriveHandler") - require.NoError(t, err) - defer func() { - os.RemoveAll(tempPath) - }() - - logger := log.G(context.Background()) - handler, err := newStubDriveHandler(tempPath, logger, 5) - assert.NoError(t, err) - assert.Equal(t, 5, len(handler.GetDrives())) - - infos, err := ioutil.ReadDir(tempPath) - assert.NoError(t, err) - assert.Equal(t, 5, len(infos)) +type MockDriveMounter struct { + drivemount.DriveMounterService + + t *testing.T + expectedDestinationPath string + expectedFilesystemType string + expectedOptions []string +} + +func (m *MockDriveMounter) MountDrive(ctx context.Context, req *drivemount.MountDriveRequest) (*empty.Empty, error) { + assert.Equal(m.t, m.expectedDestinationPath, req.DestinationPath) + assert.Equal(m.t, m.expectedFilesystemType, req.FilesytemType) + assert.Equal(m.t, m.expectedOptions, req.Options) + return nil, nil } -func TestPatchStubDrive(t *testing.T) { +func TestContainerStubs(t *testing.T) { ctx := context.Background() - index := 0 - expectedReplacements := []string{ - "/correct/path0", - "/correct/path1", - "/correct/path2", - } + logger := log.G(ctx) - mockClient := &fctesting.MockClient{ - PatchGuestDriveByIDFn: func(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) { - assert.Equal(t, expectedReplacements[index], firecracker.StringValue(params.Body.PathOnHost)) - index++ + tmpDir, err := ioutil.TempDir("", "TestCreateContainerStubs") + require.NoError(t, err, "failed to create temporary directory") + defer os.RemoveAll(tmpDir) - return nil, nil - }, - } + stubDir := filepath.Join(tmpDir, "stubs") + err = os.MkdirAll(stubDir, 0700) + require.NoError(t, err, "failed to create stub dir") - fcClient := firecracker.NewClient("/path/to/socket", nil, false, firecracker.WithOpsClient(mockClient)) - client, err := firecracker.NewMachine(ctx, firecracker.Config{}, firecracker.WithClient(fcClient)) - assert.NoError(t, err, "failed to create new machine") + patchedSrcDir := filepath.Join(tmpDir, "patchedSrcs") + err = os.MkdirAll(patchedSrcDir, 0700) + require.NoError(t, err, "failed to create patched src dir") - handler := stubDriveHandler{ - drives: []models.Drive{ - { - DriveID: firecracker.String("stub0"), - PathOnHost: firecracker.String("/fake/stub/path0"), - }, - { - DriveID: firecracker.String("stub1"), - PathOnHost: firecracker.String("/fake/stub/path1"), - }, - { - DriveID: firecracker.String("stub2"), - PathOnHost: firecracker.String("/fake/stub/path2"), - }, - }, + noopJailer := noopJailer{ + shimDir: vm.Dir(stubDir), + ctx: ctx, + logger: logger, } - expectedDriveIDs := []string{ - "stub0", - "stub1", - "stub2", - } + containerCount := 8 + machineCfg := &firecracker.Config{} - for i, path := range expectedReplacements { - driveID, err := handler.PatchStubDrive(ctx, client, path) - assert.NoError(t, err, "failed to patch stub drive") - assert.Equal(t, expectedDriveIDs[i], driveID, "drive ids are not equal") + stubDriveHandler, err := CreateContainerStubs(machineCfg, noopJailer, containerCount, logger) + require.NoError(t, err, "failed to create stub drive handler") + dirents, err := ioutil.ReadDir(stubDir) + require.NoError(t, err, "failed to read stub drive dir") + assert.Len(t, dirents, containerCount) + + for i := 0; i < containerCount; i++ { + fcDrive := machineCfg.Drives[i] + assert.False(t, firecracker.BoolValue(fcDrive.IsReadOnly)) + assert.Nil(t, fcDrive.RateLimiter) + + id := strconv.Itoa(i) + hostPath := filepath.Join(patchedSrcDir, id) + vmPath := filepath.Join("/", id) + fsType := "foo4" + fsOptions := []string{"blah", "bleg"} + + mountableStubDrive, err := stubDriveHandler.Reserve( + id, hostPath, vmPath, fsType, fsOptions) + assert.NoError(t, err, "failed to reserve stub drive") + + stub := mountableStubDrive.(stubDrive) + assert.Equal(t, hostPath, stub.driveMount.HostPath) + assert.Equal(t, vmPath, stub.driveMount.VMPath) + assert.Equal(t, fsType, stub.driveMount.FilesystemType) + assert.Equal(t, append(fsOptions, "rw"), stub.driveMount.Options) + assert.True(t, stub.driveMount.IsWritable) + assert.Nil(t, stub.driveMount.RateLimiter) + + mockMachine, err := firecracker.NewMachine(ctx, firecracker.Config{}, firecracker.WithClient( + firecracker.NewClient("/path/to/socket", nil, false, firecracker.WithOpsClient(&fctesting.MockClient{ + PatchGuestDriveByIDFn: func(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) { + assert.Equal(t, hostPath, firecracker.StringValue(params.Body.PathOnHost)) + return nil, nil + }, + })))) + require.NoError(t, err, "failed to create new machine") + + mockDriveMounter := &MockDriveMounter{ + t: t, + expectedDestinationPath: vmPath, + expectedFilesystemType: fsType, + expectedOptions: append(fsOptions, "rw"), + } + + err = mountableStubDrive.PatchAndMount(ctx, mockMachine, mockDriveMounter) + assert.NoError(t, err, "failed to patch and mount stub drive") } } -func TestPatchStubDrive_concurrency(t *testing.T) { +func TestDriveMountStubs(t *testing.T) { ctx := context.Background() - mockClient := &fctesting.MockClient{ - PatchGuestDriveByIDFn: func(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) { - return nil, nil - }, - } + logger := log.G(ctx) - fcClient := firecracker.NewClient("/path/to/socket", nil, false, firecracker.WithOpsClient(mockClient)) - client, err := firecracker.NewMachine(ctx, firecracker.Config{}, firecracker.WithClient(fcClient)) - assert.NoError(t, err, "failed to create new machine") + tmpDir, err := ioutil.TempDir("", "Test-CreateContainerStubs") + require.NoError(t, err, "failed to create temporary directory") + defer os.RemoveAll(tmpDir) - handler := stubDriveHandler{ - drives: []models.Drive{ - { - DriveID: firecracker.String("stub0"), - PathOnHost: firecracker.String("/fake/stub/path0"), - }, - { - DriveID: firecracker.String("stub1"), - PathOnHost: firecracker.String("/fake/stub/path1"), - }, - { - DriveID: firecracker.String("stub2"), - PathOnHost: firecracker.String("/fake/stub/path2"), - }, - { - DriveID: firecracker.String("stub3"), - PathOnHost: firecracker.String("/fake/stub/path3"), - }, - { - DriveID: firecracker.String("stub4"), - PathOnHost: firecracker.String("/fake/stub/path4"), - }, - { - DriveID: firecracker.String("stub5"), - PathOnHost: firecracker.String("/fake/stub/path5"), - }, - { - DriveID: firecracker.String("stub6"), - PathOnHost: firecracker.String("/fake/stub/path6"), - }, - { - DriveID: firecracker.String("stub7"), - PathOnHost: firecracker.String("/fake/stub/path7"), - }, - { - DriveID: firecracker.String("stub8"), - PathOnHost: firecracker.String("/fake/stub/path8"), - }, - { - DriveID: firecracker.String("stub9"), - PathOnHost: firecracker.String("/fake/stub/path9"), - }, - { - DriveID: firecracker.String("stub10"), - PathOnHost: firecracker.String("/fake/stub/path10"), + stubDir := filepath.Join(tmpDir, "stubs") + err = os.MkdirAll(stubDir, 0700) + require.NoError(t, err, "failed to create stub dir") + + patchedSrcDir := filepath.Join(tmpDir, "patchedSrcs") + err = os.MkdirAll(patchedSrcDir, 0700) + require.NoError(t, err, "failed to create patched src dir") + + noopJailer := noopJailer{ + shimDir: vm.Dir(stubDir), + ctx: ctx, + logger: logger, + } + + machineCfg := &firecracker.Config{} + inputDriveMounts := []*proto.FirecrackerDriveMount{ + { + HostPath: "/foo/1", + VMPath: "/bar/1", + FilesystemType: "blah1", + Options: []string{"blerg1"}, + RateLimiter: nil, + IsWritable: false, + }, + { + HostPath: "/foo/2", + VMPath: "/bar/2", + FilesystemType: "blah2", + Options: []string{"blerg2"}, + RateLimiter: nil, + IsWritable: true, + }, + { + HostPath: "/foo/3", + VMPath: "/bar/3", + FilesystemType: "blah3", + Options: []string{"blerg3"}, + RateLimiter: &proto.FirecrackerRateLimiter{ + Bandwidth: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 111, + RefillTime: 222, + Capacity: 333, + }, + Ops: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 1111, + RefillTime: 2222, + Capacity: 3333, + }, }, - { - DriveID: firecracker.String("stub11"), - PathOnHost: firecracker.String("/fake/stub/path11"), + IsWritable: false, + }, + { + HostPath: "/foo/4", + VMPath: "/bar/4", + FilesystemType: "blah4", + Options: []string{"blerg4"}, + RateLimiter: &proto.FirecrackerRateLimiter{ + Bandwidth: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 999, + RefillTime: 888, + Capacity: 777, + }, + Ops: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 9999, + RefillTime: 8888, + Capacity: 7777, + }, }, + IsWritable: true, }, } - replacementPaths := []string{ - "/correct/path0", - "/correct/path1", - "/correct/path2", - "/correct/path3", - "/correct/path4", - "/correct/path5", - "/correct/path6", - "/correct/path7", - "/correct/path8", - "/correct/path9", - "/correct/path10", - "/correct/path11", - } - var wg sync.WaitGroup - wg.Add(len(replacementPaths)) - for _, path := range replacementPaths { - go func(path string) { - defer wg.Done() - _, err := handler.PatchStubDrive(ctx, client, path) - assert.NoError(t, err, "failed to patch stub drive") - }(path) - } + mountableStubDrives, err := CreateDriveMountStubs(machineCfg, noopJailer, inputDriveMounts, logger) + require.NoError(t, err, "failed to create stub drive handler") - wg.Wait() + dirents, err := ioutil.ReadDir(stubDir) + require.NoError(t, err, "failed to read stub drive dir") + assert.Len(t, dirents, len(inputDriveMounts)) - validPaths := map[string]struct{}{} - for _, path := range replacementPaths { - validPaths[path] = struct{}{} - } + for i, mountableStubDrive := range mountableStubDrives { + inputDriveMount := inputDriveMounts[i] + hostPath := inputDriveMount.HostPath + vmPath := inputDriveMount.VMPath + fsType := inputDriveMount.FilesystemType + rateLimiter := inputDriveMount.RateLimiter + isWritable := inputDriveMount.IsWritable - assert.Equal(t, len(validPaths), len(handler.drives), "incorrect drive amount") - for _, drive := range handler.drives { - path := firecracker.StringValue(drive.PathOnHost) - _, ok := validPaths[path] - assert.True(t, ok, "path was not in valid path map") - delete(validPaths, path) - } + fsOptions := inputDriveMount.Options + if isWritable { + fsOptions = append(fsOptions, "rw") + } else { + fsOptions = append(fsOptions, "ro") + } -} + fcDrive := machineCfg.Drives[i] + assert.Equal(t, !isWritable, firecracker.BoolValue(fcDrive.IsReadOnly)) + assert.Equal(t, rateLimiterFromProto(rateLimiter), fcDrive.RateLimiter) -func TestCreateStubDrive(t *testing.T) { - cases := []struct { - Name string - DriveID string - ExpectedSize int64 - ExpectedError bool - }{ - { - Name: "valid case", - DriveID: "foo", - ExpectedSize: fcSectorSize, - }, - { - Name: "residual bytes case", - DriveID: strings.Repeat("0", 0xFF), - ExpectedSize: fcSectorSize, - }, + stub := mountableStubDrive.(stubDrive) + assert.Equal(t, hostPath, stub.driveMount.HostPath) + assert.Equal(t, vmPath, stub.driveMount.VMPath) + assert.Equal(t, fsType, stub.driveMount.FilesystemType) + assert.Equal(t, fsOptions, stub.driveMount.Options) + assert.Equal(t, rateLimiter, stub.driveMount.RateLimiter) + assert.Equal(t, isWritable, stub.driveMount.IsWritable) + + mockMachine, err := firecracker.NewMachine(ctx, firecracker.Config{}, firecracker.WithClient( + firecracker.NewClient("/path/to/socket", nil, false, firecracker.WithOpsClient(&fctesting.MockClient{ + PatchGuestDriveByIDFn: func(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) { + assert.Equal(t, hostPath, firecracker.StringValue(params.Body.PathOnHost)) + return nil, nil + }, + })))) + require.NoError(t, err, "failed to create new machine") + + mockDriveMounter := &MockDriveMounter{ + t: t, + expectedDestinationPath: vmPath, + expectedFilesystemType: fsType, + expectedOptions: fsOptions, + } + + err = mountableStubDrive.PatchAndMount(ctx, mockMachine, mockDriveMounter) + assert.NoError(t, err, "failed to patch and mount stub drive") } +} - tmpDir := os.TempDir() - path, err := ioutil.TempDir(tmpDir, "TestCreateStubDrive") - assert.NoError(t, err, "failed to create test directory") - defer os.RemoveAll(path) - - for _, c := range cases { - c := c // see https://github.com/kyoh86/scopelint/issues/4 - t.Run(c.Name, func(t *testing.T) { - logger := log.G(context.Background()) - handler, err := newStubDriveHandler(path, logger, 0) - assert.NoError(t, err) - - stubDrivePath := filepath.Join(path, c.Name) - err = handler.createStubDrive(c.DriveID, stubDrivePath) - assert.Equal(t, c.ExpectedError, err != nil, "invalid error: %v", err) - - info, err := os.Stat(stubDrivePath) - assert.NoError(t, err, "failed to stat %v", stubDrivePath) - assert.Equal(t, c.ExpectedSize, info.Size(), "mismatch of sizes") - }) +func TestStubPathToDriveID(t *testing.T) { + for _, stubPath := range []string{ + "/a/b/c", + "foo-bar", + } { + assert.Regexp(t, `^[a-zA-Z0-9_]*$`, stubPathToDriveID(stubPath), + "unexpected invalid characters in drive ID") } } diff --git a/runtime/helpers.go b/runtime/helpers.go index 99be1b6d9..63de86d91 100644 --- a/runtime/helpers.go +++ b/runtime/helpers.go @@ -115,6 +115,10 @@ func networkConfigFromProto(nwIface *proto.FirecrackerNetworkInterface, vmID str // rateLimiterFromProto creates a firecracker RateLimiter object from the // protobuf message. func rateLimiterFromProto(rl *proto.FirecrackerRateLimiter) *models.RateLimiter { + if rl == nil { + return nil + } + result := models.RateLimiter{} if rl.Bandwidth != nil { result.Bandwidth = tokenBucketFromProto(rl.Bandwidth) diff --git a/runtime/jailer.go b/runtime/jailer.go index 6b543d1c0..a37c3997e 100644 --- a/runtime/jailer.go +++ b/runtime/jailer.go @@ -15,6 +15,7 @@ package main import ( "context" + "os" "github.com/firecracker-microvm/firecracker-go-sdk" "github.com/sirupsen/logrus" @@ -54,14 +55,18 @@ type jailer interface { // JailPath is used to return the directory we are supposed to be working in. JailPath() vm.Dir // StubDrivesOptions will return a set of options used to create a new stub - // drive handler. - StubDrivesOptions() []stubDrivesOpt + // drive file + StubDrivesOptions() []FileOpt } type cgroupPather interface { CgroupPath() string } +// FileOpt is a functional option that operates on an open file, modifying it to be usable +// by the jailer implementation providing the option. +type FileOpt func(*os.File) error + // newJailer is used to construct a jailer from the CreateVM request. If no // request or jailer config was provided, then the noopJailer will be returned. func newJailer( diff --git a/runtime/noop_jailer.go b/runtime/noop_jailer.go index 516b032b3..6513e0a45 100644 --- a/runtime/noop_jailer.go +++ b/runtime/noop_jailer.go @@ -73,7 +73,7 @@ func (j noopJailer) ExposeFileToJail(path string) error { return nil } -func (j noopJailer) StubDrivesOptions() []stubDrivesOpt { +func (j noopJailer) StubDrivesOptions() []FileOpt { j.logger.Debug("noop operation for StubDrivesOptions") - return []stubDrivesOpt{} + return []FileOpt{} } diff --git a/runtime/runc_jailer.go b/runtime/runc_jailer.go index f1790b98e..2f0ec31ad 100644 --- a/runtime/runc_jailer.go +++ b/runtime/runc_jailer.go @@ -26,10 +26,10 @@ import ( "syscall" "github.com/firecracker-microvm/firecracker-go-sdk" - models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" "github.com/firecracker-microvm/firecracker-containerd/internal" "github.com/firecracker-microvm/firecracker-containerd/internal/vm" @@ -265,14 +265,12 @@ func (j *runcJailer) BuildLinkFifoHandler() firecracker.Handler { // StubDrivesOptions will return a set of options used to create a new stub // drive handler. -func (j *runcJailer) StubDrivesOptions() []stubDrivesOpt { - return []stubDrivesOpt{ - func(drives []models.Drive) error { - for _, drive := range drives { - path := firecracker.StringValue(drive.PathOnHost) - if err := os.Chown(path, int(j.uid), int(j.gid)); err != nil { - return err - } +func (j runcJailer) StubDrivesOptions() []FileOpt { + return []FileOpt{ + func(file *os.File) error { + err := unix.Fchown(int(file.Fd()), int(j.uid), int(j.gid)) + if err != nil { + return errors.Wrapf(err, "failed to chown stub file %q", file.Name()) } return nil }, diff --git a/runtime/service.go b/runtime/service.go index c8f27cc1f..69ec93956 100644 --- a/runtime/service.go +++ b/runtime/service.go @@ -124,15 +124,15 @@ type service struct { agentClient taskAPI.TaskService eventBridgeClient eventbridge.Getter driveMountClient drivemount.DriveMounterService - stubDriveHandler *stubDriveHandler + jailer jailer + containerStubHandler *StubDriveHandler + driveMountStubs []MountableStubDrive exitAfterAllTasksDeleted bool // exit the VM and shim when all tasks are deleted machine *firecracker.Machine machineConfig *firecracker.Config vsockIOPortCount uint32 vsockPortMu sync.Mutex - - jailer jailer } func shimOpts(shimCtx context.Context) (*shim.Opts, error) { @@ -520,7 +520,7 @@ func (s *service) createVM(requestCtx context.Context, request *proto.CreateVMRe s.driveMountClient = drivemount.NewDriveMounterClient(rpcClient) s.exitAfterAllTasksDeleted = request.ExitAfterAllTasksDeleted - err = s.mountDrives(requestCtx, request.DriveMounts) + err = s.mountDrives(requestCtx) if err != nil { return err } @@ -529,27 +529,12 @@ func (s *service) createVM(requestCtx context.Context, request *proto.CreateVMRe return nil } -func (s *service) mountDrives(requestCtx context.Context, driveMounts []*proto.FirecrackerDriveMount) error { - for _, driveMount := range driveMounts { - err := s.jailer.ExposeFileToJail(driveMount.HostPath) - if err != nil { - return errors.Wrapf(err, "failed to expose drive mount host path to jail %s", driveMount.HostPath) - } - - driveID, err := s.stubDriveHandler.PatchStubDrive(requestCtx, s.machine, driveMount.HostPath) +func (s *service) mountDrives(requestCtx context.Context) error { + for _, stubDrive := range s.driveMountStubs { + err := stubDrive.PatchAndMount(requestCtx, s.machine, s.driveMountClient) if err != nil { return errors.Wrapf(err, "failed to patch drive mount stub") } - - _, err = s.driveMountClient.MountDrive(requestCtx, &drivemount.MountDriveRequest{ - DriveID: driveID, - DestinationPath: driveMount.VMPath, - FilesytemType: driveMount.FilesystemType, - Options: driveMount.Options, - }) - if err != nil { - return errors.Wrapf(err, "failed to mount drive inside vm") - } } return nil } @@ -723,6 +708,8 @@ func (s *service) buildVMConfiguration(req *proto.CreateVMRequest) (*firecracker cfg.KernelImagePath = s.config.KernelImagePath } + cfg.Drives = s.buildRootDrive(req) + // Drives configuration containerCount := int(req.ContainerCount) if containerCount < 1 { @@ -732,18 +719,17 @@ func (s *service) buildVMConfiguration(req *proto.CreateVMRequest) (*firecracker containerCount = 1 } - // Create stub drives first and let stub driver handler manage the drives - stubDriveHandler, err := newStubDriveHandler( - string(s.jailer.JailPath()), - s.logger, containerCount+len(req.DriveMounts), - s.jailer.StubDrivesOptions()..., - ) + s.containerStubHandler, err = CreateContainerStubs( + &cfg, s.jailer, containerCount, s.logger) if err != nil { - return nil, errors.Wrap(err, "failed to create stub drives") + return nil, errors.Wrapf(err, "failed to create container stub drives") } - s.stubDriveHandler = stubDriveHandler - cfg.Drives = append(stubDriveHandler.GetDrives(), s.buildRootDrive(req)...) + s.driveMountStubs, err = CreateDriveMountStubs( + &cfg, s.jailer, req.DriveMounts, s.logger) + if err != nil { + return nil, errors.Wrapf(err, "failed to create drive mount stub drives") + } // If no value for NetworkInterfaces was specified (not even an empty but non-nil list) and // the runtime config specifies a default list, use those defaults @@ -817,28 +803,26 @@ func (s *service) Create(requestCtx context.Context, request *taskAPI.CreateTask return nil, err } - for _, mnt := range request.Rootfs { - if err := s.jailer.ExposeFileToJail(mnt.Source); err != nil { - return nil, errors.Wrapf(err, "failed to expose mount to jail %v", mnt.Source) - } + // We don't support a rootfs with multiple mounts, only one mount can be exposed to the + // vm per-container + if len(request.Rootfs) != 1 { + return nil, errors.Errorf("can only support rootfs with exactly one mount: %+v", request.Rootfs) + } + rootfsMnt := request.Rootfs[0] - driveID, err := s.stubDriveHandler.PatchStubDrive(requestCtx, s.machine, mnt.Source) - if err != nil { - if err == ErrDrivesExhausted { - return nil, errors.Wrapf(errdefs.ErrUnavailable, "no remaining stub drives to be used") - } - return nil, errors.Wrapf(err, "failed to mount %v", mnt.Source) - } + stubDrive, err := s.containerStubHandler.Reserve(request.ID, + rootfsMnt.Source, vmBundleDir.RootfsPath(), "ext4", nil) + if err != nil { + err = errors.Wrapf(err, "failed to get stub drive for task %q", request.ID) + logger.WithError(err).Error() + return nil, err + } - _, err = s.driveMountClient.MountDrive(requestCtx, &drivemount.MountDriveRequest{ - DriveID: driveID, - DestinationPath: vmBundleDir.RootfsPath(), - FilesytemType: "ext4", - Options: nil, - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to mount drive inside vm") - } + err = stubDrive.PatchAndMount(requestCtx, s.machine, s.driveMountClient) + if err != nil { + err = errors.Wrapf(err, "failed to mount drive inside vm") + logger.WithError(err).Error() + return nil, err } ociConfigBytes, err := hostBundleDir.OCIConfig().Bytes() diff --git a/runtime/service_integ_test.go b/runtime/service_integ_test.go index 27ea346f7..4cf628209 100644 --- a/runtime/service_integ_test.go +++ b/runtime/service_integ_test.go @@ -812,7 +812,7 @@ func TestCreateTooManyContainers_Isolated(t *testing.T) { // When we reuse a VM explicitly, we cannot start multiple containers unless we pre-allocate stub drives. _, err = c2.NewTask(ctx, cio.NewCreator(cio.WithStreams(nil, &stdout, &stderr))) - assert.Equal("no remaining stub drives to be used: unavailable: unknown", err.Error()) + assert.Contains(err.Error(), "There are no remaining drives to be used") require.Error(t, err) } @@ -842,6 +842,8 @@ func TestDriveMount_Isolated(t *testing.T) { VMMountOptions []string ContainerPath string FSImgFile internal.FSImgFile + IsWritable bool + RateLimiter *proto.FirecrackerRateLimiter }{ { // /systemmount meant to make sure logic doesn't ban this just because it begins with /sys @@ -853,28 +855,48 @@ func TestDriveMount_Isolated(t *testing.T) { Subpath: "dir/foo", Contents: "foo\n", }, + RateLimiter: &proto.FirecrackerRateLimiter{ + Bandwidth: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 111, + RefillTime: 222, + Capacity: 333, + }, + Ops: &proto.FirecrackerTokenBucket{ + OneTimeBurst: 1111, + RefillTime: 2222, + Capacity: 3333, + }, + }, + IsWritable: true, }, { VMPath: "/mnt", FilesystemType: "ext3", - VMMountOptions: []string{"ro", "relatime"}, + // don't specify "ro" to validate it's automatically set via "IsWritable: false" + VMMountOptions: []string{"relatime"}, ContainerPath: "/bar", FSImgFile: internal.FSImgFile{ Subpath: "dir/bar", Contents: "bar\n", }, + // you actually get permission denied if you try to mount a ReadOnly block device + // w/ "rw" mount option, so we can only test IsWritable=false when "ro" is also the + // mount option, not in isolation + IsWritable: false, }, } vmDriveMounts := []*proto.FirecrackerDriveMount{} ctrBindMounts := []specs.Mount{} - ctrCatCommands := []string{} + ctrCommands := []string{} for _, vmMount := range vmMounts { vmDriveMounts = append(vmDriveMounts, &proto.FirecrackerDriveMount{ HostPath: internal.CreateFSImg(ctx, t, vmMount.FilesystemType, vmMount.FSImgFile), VMPath: vmMount.VMPath, FilesystemType: vmMount.FilesystemType, Options: vmMount.VMMountOptions, + IsWritable: vmMount.IsWritable, + RateLimiter: vmMount.RateLimiter, }) ctrBindMounts = append(ctrBindMounts, specs.Mount{ @@ -883,9 +905,20 @@ func TestDriveMount_Isolated(t *testing.T) { Options: []string{"bind"}, }) - ctrCatCommands = append(ctrCatCommands, fmt.Sprintf("/bin/cat %s", + ctrCommands = append(ctrCommands, fmt.Sprintf("/bin/cat %s", filepath.Join(vmMount.ContainerPath, vmMount.FSImgFile.Subpath), )) + + if !vmMount.IsWritable { + // if read-only is set on the firecracker drive, make sure that you are unable + // to create a new file + ctrCommands = append(ctrCommands, fmt.Sprintf(`/bin/sh -c '/bin/touch %s 2>/dev/null && exit 1 || exit 0'`, + filepath.Join(vmMount.ContainerPath, vmMount.FSImgFile.Subpath+"noexist"), + )) + } + + // RateLimiter settings are not asserted on in this test right now as there's not a clear simple + // way to test them. Coverage that RateLimiter settings are passed as expected are covered in unit tests } _, err = fcClient.CreateVM(ctx, &proto.CreateVMRequest{ @@ -903,7 +936,7 @@ func TestDriveMount_Isolated(t *testing.T) { containerd.WithSnapshotter(defaultSnapshotterName()), containerd.WithNewSnapshot(snapshotName, image), containerd.WithNewSpec( - oci.WithProcessArgs("/bin/sh", "-c", strings.Join(append(ctrCatCommands, + oci.WithProcessArgs("/bin/sh", "-c", strings.Join(append(ctrCommands, "/bin/cat /proc/mounts", ), " && ")), oci.WithMounts(ctrBindMounts), @@ -914,7 +947,7 @@ func TestDriveMount_Isolated(t *testing.T) { outputLines := strings.Split(startAndWaitTask(ctx, t, newContainer), "\n") if len(outputLines) < len(vmMounts) { - require.Fail(t, "unexpected ctr output, expected at least %d lines: %+v", len(vmMounts), outputLines) + require.Fail(t, "unexpected ctr output", "expected at least %d lines: %+v", len(vmMounts), outputLines) } mountInfos, err := internal.ParseProcMountLines(outputLines[len(vmMounts):]...) @@ -938,6 +971,13 @@ func TestDriveMount_Isolated(t *testing.T) { assert.Containsf(t, actualMountInfo.Options, vmMountOption, "vm mount at %q did not have expected option", vmMount.ContainerPath) } + if !vmMount.IsWritable { + assert.Containsf(t, actualMountInfo.Options, "ro", + `vm mount at %q with IsWritable=false did not have "ro" option`, vmMount.ContainerPath) + } else { + assert.Containsf(t, actualMountInfo.Options, "rw", + `vm mount at %q with IsWritable=false did not have "rw" option`, vmMount.ContainerPath) + } break } } @@ -976,6 +1016,21 @@ func TestDriveMountFails_Isolated(t *testing.T) { VMPath: "/sys/foo", // invalid due to being under /sys FilesystemType: "ext4", }, + { + HostPath: testImgHostPath, + VMPath: "/valid", + FilesystemType: "ext4", + // invalid due to "ro" option used with IsWritable=true + Options: []string{"ro"}, + IsWritable: true, + }, + { + HostPath: testImgHostPath, + VMPath: "/valid", + FilesystemType: "ext4", + // invalid due to "rw" option used with IsWritable=false + Options: []string{"rw"}, + }, } { _, err = fcClient.CreateVM(ctx, &proto.CreateVMRequest{ VMID: "test-drive-mount-fails", @@ -984,7 +1039,7 @@ func TestDriveMountFails_Isolated(t *testing.T) { // TODO it would be good to check for more specific error types, see #294 for possible improvements: // https://github.com/firecracker-microvm/firecracker-containerd/issues/294 - assert.Error(t, err, "unexpectedly succeeded in creating a VM with a drive mount under banned path") + assert.Error(t, err, "unexpectedly succeeded in creating a VM with an invalid drive mount") } } diff --git a/runtime/service_test.go b/runtime/service_test.go index 30fa29ae1..48c5c8486 100644 --- a/runtime/service_test.go +++ b/runtime/service_test.go @@ -250,18 +250,17 @@ func TestBuildVMConfiguration(t *testing.T) { drives := make([]models.Drive, tc.expectedStubDriveCount) for i := 0; i < tc.expectedStubDriveCount; i++ { - drives[i].PathOnHost = firecracker.String(filepath.Join(tempDir, fmt.Sprintf("stub%d", i))) - drives[i].DriveID = firecracker.String(fmt.Sprintf("stub%d", i)) + hostPath := filepath.Join(tempDir, fmt.Sprintf("ctrstub%d", i)) + drives[i].PathOnHost = firecracker.String(hostPath) + drives[i].DriveID = firecracker.String(stubPathToDriveID(hostPath)) drives[i].IsReadOnly = firecracker.Bool(false) drives[i].IsRootDevice = firecracker.Bool(false) } - tc.expectedCfg.Drives = append(drives, tc.expectedCfg.Drives...) + tc.expectedCfg.Drives = append(tc.expectedCfg.Drives, drives...) actualCfg, err := svc.buildVMConfiguration(tc.request) assert.NoError(t, err) require.Equal(t, tc.expectedCfg, actualCfg) - - require.Equal(t, tc.expectedStubDriveCount, len(svc.stubDriveHandler.drives), "The stub driver only knows stub drives") }) } }