-
Notifications
You must be signed in to change notification settings - Fork 275
Expand file tree
/
Copy pathVZVirtualMachineInstance.swift
More file actions
403 lines (354 loc) · 13.4 KB
/
VZVirtualMachineInstance.swift
File metadata and controls
403 lines (354 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
#if os(macOS)
import Foundation
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import Logging
import NIOCore
import NIOPosix
import Synchronization
import Virtualization
struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
typealias Agent = Vminitd
/// Attached mounts on the sandbox.
public let mounts: [AttachedFilesystem]
/// Returns the runtime state of the vm.
public var state: VirtualMachineInstanceState {
vzStateToInstanceState()
}
/// The sandbox configuration.
private let config: Configuration
public struct Configuration: Sendable {
/// Amount of cpus to allocated.
public var cpus: Int
/// Amount of memory in bytes allocated.
public var memoryInBytes: UInt64
/// Toggle rosetta's x86_64 emulation support.
public var rosetta: Bool
/// Toggle nested virtualization support.
public var nestedVirtualization: Bool
/// Mount attachments.
public var mounts: [Mount]
/// Network interface attachments.
public var interfaces: [any Interface]
/// Kernel image.
public var kernel: Kernel?
/// The root filesystem.
public var initialFilesystem: Mount?
/// File path to store the sandbox boot logs.
public var bootlog: URL?
/// Set of virtiofs tags that have already been configured to avoid duplicates.
var usedVirtioFSTags: Set<String> = []
init() {
self.cpus = 4
self.memoryInBytes = 1024.mib()
self.rosetta = false
self.nestedVirtualization = false
self.mounts = []
self.interfaces = []
self.usedVirtioFSTags = []
}
}
private nonisolated(unsafe) let vm: VZVirtualMachine
private let queue: DispatchQueue
private let group: MultiThreadedEventLoopGroup
private let lock: AsyncLock
private let timeSyncer: TimeSyncer
private let logger: Logger?
public init(
group: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount),
logger: Logger? = nil,
with: (inout Configuration) throws -> Void
) throws {
var config = Configuration()
try with(&config)
try self.init(group: group, config: config, logger: logger)
}
init(group: MultiThreadedEventLoopGroup, config: Configuration, logger: Logger?) throws {
var mutableConfig = config
self.config = config
self.group = group
self.lock = .init()
self.queue = DispatchQueue(label: "com.apple.containerization.sandbox.\(UUID().uuidString)")
self.mounts = try config.mountAttachments()
self.logger = logger
self.timeSyncer = .init(logger: logger)
self.vm = VZVirtualMachine(
configuration: try mutableConfig.toVZ(),
queue: self.queue
)
}
}
extension VZVirtualMachineInstance {
func vzStateToInstanceState() -> VirtualMachineInstanceState {
self.queue.sync {
let state: VirtualMachineInstanceState
switch self.vm.state {
case .starting:
state = .starting
case .running:
state = .running
case .stopping:
state = .stopping
case .stopped:
state = .stopped
default:
state = .unknown
}
return state
}
}
func start() async throws {
try await lock.withLock { _ in
guard self.state == .stopped else {
throw ContainerizationError(
.invalidState,
message: "sandbox is not stopped \(self.state)"
)
}
// Do any necessary setup needed prior to starting the guest.
try await self.prestart()
try await self.vm.start(queue: self.queue)
let agent = Vminitd(
connection: try await self.vm.waitForAgent(queue: self.queue),
group: self.group
)
do {
if self.config.rosetta {
try await agent.enableRosetta()
}
} catch {
try await agent.close()
throw error
}
// Don't close our remote context as we are providing
// it to our time sync routine.
await self.timeSyncer.start(context: agent)
}
}
func stop() async throws {
try await lock.withLock { _ in
// NOTE: We should record HOW the vm stopped eventually. If the vm exited
// unexpectedly virtualization framework offers you a way to store
// an error on how it exited. We should report that here instead of the
// generic vm is not running.
guard self.state == .running else {
throw ContainerizationError(.invalidState, message: "vm is not running")
}
try await self.timeSyncer.close()
try await self.vm.stop(queue: self.queue)
try await self.group.shutdownGracefully()
}
}
public func dialAgent() async throws -> Vminitd {
let conn = try await dial(Vminitd.port)
return Vminitd(connection: conn, group: self.group)
}
}
extension VZVirtualMachineInstance {
func dial(_ port: UInt32) async throws -> FileHandle {
try await vm.connect(
queue: queue,
port: port
).dupHandle()
}
func listen(_ port: UInt32) throws -> VsockConnectionStream {
let stream = VsockConnectionStream(port: port)
let listener = VZVirtioSocketListener()
listener.delegate = stream
try self.vm.listen(
queue: queue,
port: port,
listener: listener
)
return stream
}
func stopListen(_ port: UInt32) throws {
try self.vm.removeListener(
queue: queue,
port: port
)
}
func prestart() async throws {
if self.config.rosetta {
#if arch(arm64)
if VZLinuxRosettaDirectoryShare.availability == .notInstalled {
self.logger?.info("installing rosetta")
try await VZVirtualMachineInstance.Configuration.installRosetta()
}
#else
fatalError("rosetta is only supported on arm64")
#endif
}
}
}
extension VZVirtualMachineInstance.Configuration {
public static func installRosetta() async throws {
do {
#if arch(arm64)
try await VZLinuxRosettaDirectoryShare.installRosetta()
#else
fatalError("rosetta is only supported on arm64")
#endif
} catch {
throw ContainerizationError(
.internalError,
message: "failed to install rosetta",
cause: error
)
}
}
private func serialPort(path: URL) throws -> [VZVirtioConsoleDeviceSerialPortConfiguration] {
let c = VZVirtioConsoleDeviceSerialPortConfiguration()
c.attachment = try VZFileSerialPortAttachment(url: path, append: true)
return [c]
}
mutating func toVZ() throws -> VZVirtualMachineConfiguration {
var config = VZVirtualMachineConfiguration()
config.cpuCount = self.cpus
config.memorySize = self.memoryInBytes
config.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
if let bootlog = self.bootlog {
config.serialPorts = try serialPort(path: bootlog)
}
config.networkDevices = try self.interfaces.map {
guard let vzi = $0 as? VZInterface else {
throw ContainerizationError(.invalidArgument, message: "interface type not supported by VZ")
}
return try vzi.device()
}
if self.rosetta {
#if arch(arm64)
switch VZLinuxRosettaDirectoryShare.availability {
case .notSupported:
throw ContainerizationError(
.invalidArgument,
message: "rosetta was requested but is not supported on this machine"
)
case .notInstalled:
// NOTE: If rosetta isn't installed, we'll error with a nice error message
// during .start() of the virtual machine instance.
fallthrough
case .installed:
let share = try VZLinuxRosettaDirectoryShare()
let device = VZVirtioFileSystemDeviceConfiguration(tag: "rosetta")
device.share = share
config.directorySharingDevices.append(device)
@unknown default:
throw ContainerizationError(
.invalidArgument,
message: "unknown rosetta availability encountered: \(VZLinuxRosettaDirectoryShare.availability)"
)
}
#else
fatalError("rosetta is only supported on arm64")
#endif
}
guard let kernel = self.kernel else {
throw ContainerizationError(.invalidArgument, message: "kernel cannot be nil")
}
guard let initialFilesystem = self.initialFilesystem else {
throw ContainerizationError(.invalidArgument, message: "rootfs cannot be nil")
}
let loader = VZLinuxBootLoader(kernelURL: kernel.path)
loader.commandLine = kernel.linuxCommandline(initialFilesystem: initialFilesystem)
config.bootLoader = loader
try initialFilesystem.configure(config: &config, usedVirtioFSTags: &usedVirtioFSTags)
for mount in self.mounts {
try mount.configure(config: &config, usedVirtioFSTags: &usedVirtioFSTags)
}
let platform = VZGenericPlatformConfiguration()
// We shouldn't silently succeed if the user asked for virt and their hardware does
// not support it.
if !VZGenericPlatformConfiguration.isNestedVirtualizationSupported && self.nestedVirtualization {
throw ContainerizationError(
.unsupported,
message: "nested virtualization is not supported on the platform"
)
}
platform.isNestedVirtualizationEnabled = self.nestedVirtualization
config.platform = platform
try config.validate()
return config
}
func mountAttachments() throws -> [AttachedFilesystem] {
let allocator = Character.blockDeviceTagAllocator()
if let initialFilesystem {
// When the initial filesystem is a blk, allocate the first letter "vd(a)"
// as that is what this blk will be attached under.
if initialFilesystem.isBlock {
_ = try allocator.allocate()
}
}
var attachments: [AttachedFilesystem] = []
for mount in self.mounts {
attachments.append(try .init(mount: mount, allocator: allocator))
}
return attachments
}
}
extension Mount {
var isBlock: Bool {
type == "ext4"
}
}
extension Kernel {
func linuxCommandline(initialFilesystem: Mount) -> String {
var args = self.commandLine.kernelArgs
args.append("init=/sbin/vminitd")
// rootfs is always set as ro.
args.append("ro")
switch initialFilesystem.type {
case "virtiofs":
args.append(contentsOf: [
"rootfstype=virtiofs",
"root=rootfs",
])
case "ext4":
args.append(contentsOf: [
"rootfstype=ext4",
"root=/dev/vda",
])
default:
fatalError("unsupported initfs filesystem \(initialFilesystem.type)")
}
if self.commandLine.initArgs.count > 0 {
args.append("--")
args.append(contentsOf: self.commandLine.initArgs)
}
return args.joined(separator: " ")
}
}
public protocol VZInterface {
func device() throws -> VZVirtioNetworkDeviceConfiguration
}
extension NATInterface: VZInterface {
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
let config = VZVirtioNetworkDeviceConfiguration()
if let macAddress = self.macAddress {
guard let mac = VZMACAddress(string: macAddress) else {
throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
}
config.macAddress = mac
}
config.attachment = VZNATNetworkDeviceAttachment()
return config
}
}
#endif