Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested virtualization with Linux VM #933

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Sources/tart/Commands/Set.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ struct Set: AsyncParsableCommand {
@Option(help: "VM display resolution in a format of <width>x<height>. For example, 1200x800")
var display: VMDisplayConfig?

@Flag(help: ArgumentHelp("Enable nested virtualization for the VM if supported."))
var nestedVirtualization: Bool = false

@Flag(help: ArgumentHelp("Generate a new random MAC address for the VM."))
var randomMAC: Bool = false

Expand Down Expand Up @@ -74,6 +77,10 @@ struct Set: AsyncParsableCommand {
}
#endif

if nestedVirtualization {
try vmConfig.enableNestedVirtualisation()
}

try vmConfig.save(toURL: vmDir.configURL)

if let disk = disk {
Expand Down
2 changes: 1 addition & 1 deletion Sources/tart/Platform/Darwin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
VZMacOSBootLoader()
}

func platform(nvramURL: URL) throws -> VZPlatformConfiguration {
func platform(nvramURL: URL, enableNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
let result = VZMacPlatformConfiguration()

result.machineIdentifier = ecid
Expand Down
13 changes: 11 additions & 2 deletions Sources/tart/Platform/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Virtualization

@available(macOS 13, *)
struct Linux: Platform {

func os() -> OS {
.linux
}
Expand All @@ -14,8 +15,16 @@ struct Linux: Platform {
return result
}

func platform(nvramURL: URL) throws -> VZPlatformConfiguration {
VZGenericPlatformConfiguration()
func platform(nvramURL: URL, enableNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
let result: VZGenericPlatformConfiguration = VZGenericPlatformConfiguration()

if #available(macOS 15.0, *) {
if VZGenericPlatformConfiguration.isNestedVirtualizationSupported {
result.isNestedVirtualizationEnabled = enableNestedVirtualization
}
}

return result
Comment on lines +18 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems enableNestedVirtualization is not used here and I think we can revert the rest of the changes in other files and just enable virtualization if possible.

Suggested change
func platform(nvramURL: URL, enableNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
let result: VZGenericPlatformConfiguration = VZGenericPlatformConfiguration()
if #available(macOS 15.0, *) {
if VZGenericPlatformConfiguration.isNestedVirtualizationSupported {
result.isNestedVirtualizationEnabled = enableNestedVirtualization
}
}
return result
func platform(nvramURL: URL) throws -> VZPlatformConfiguration {
let result: VZGenericPlatformConfiguration = VZGenericPlatformConfiguration()
if #available(macOS 15.0, *) {
if VZGenericPlatformConfiguration.isNestedVirtualizationSupported {
result.isNestedVirtualizationEnabled = enableNestedVirtualization
}
}
return result

What do you think? Can do it in this PR or if it's more convenient in a separate one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point of view is: nested virtualization must be an user option.

lima tool do it as user option. Probably some users don't want allow the ability to create VM inside a VM in their work.

Also when nested virtualization is active extras kernel modules are load using extras memory and bandwith in IO.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point of view is: nested virtualization must be an user option.

What do you think about a --nested option to tart run then?

Copy link
Contributor Author

@Fred78290 Fred78290 Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about a --nested option to tart run then?

I agree it was my initial pull request 👍🏻

}

func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
Expand Down
2 changes: 1 addition & 1 deletion Sources/tart/Platform/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Virtualization
protocol Platform: Codable {
func os() -> OS
func bootLoader(nvramURL: URL) throws -> VZBootLoader
func platform(nvramURL: URL) throws -> VZPlatformConfiguration
func platform(nvramURL: URL, enableNestedVirtualization: Bool) throws -> VZPlatformConfiguration
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
func keyboards() -> [VZKeyboardConfiguration]
func pointingDevices() -> [VZPointingDeviceConfiguration]
Expand Down
5 changes: 3 additions & 2 deletions Sources/tart/VM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
vmDir: VMDirectory,
ipswURL: URL,
diskSizeGB: UInt16,
nestedVirtualization: Bool = false,
network: Network = NetworkShared(),
additionalStorageDevices: [VZStorageDeviceConfiguration] = [],
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
Expand Down Expand Up @@ -231,7 +232,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
try vmDir.resizeDisk(diskSizeGB)

// Create config
let config = VMConfig(platform: Linux(), cpuCountMin: 4, memorySizeMin: 4096 * 1024 * 1024)
let config = VMConfig(platform: Linux(), cpuCountMin: 1, memorySizeMin: 512 * 1024 * 1024)
try config.save(toURL: vmDir.configURL)

return try VM(vmDir: vmDir)
Expand Down Expand Up @@ -309,7 +310,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
configuration.memorySize = vmConfig.memorySize

// Platform
configuration.platform = try vmConfig.platform.platform(nvramURL: nvramURL)
configuration.platform = try vmConfig.platform.platform(nvramURL: nvramURL, enableNestedVirtualization: vmConfig.nestedVirtualization)

// Display
configuration.graphicsDevices = [vmConfig.platform.graphicsDevice(vmConfig: vmConfig)]
Expand Down
67 changes: 50 additions & 17 deletions Sources/tart/VMConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ class LessThanMinimalResourcesError: NSObject, LocalizedError {
}
}

class UnsupportedNestedVirtualizationError: NSObject, LocalizedError {
var userExplanation: String

init(_ userExplanation: String) {
self.userExplanation = userExplanation
}

override var description: String {
get {
"UnsupportedNestedVirtualizationError: \(userExplanation)"
}
}
}

enum CodingKeys: String, CodingKey {
case version
case os
Expand All @@ -24,6 +38,7 @@ enum CodingKeys: String, CodingKey {
case memorySize
case macAddress
case display
case nestedVirtualization

// macOS-specific keys
case ecid
Expand Down Expand Up @@ -52,7 +67,8 @@ struct VMConfig: Codable {
private(set) var memorySize: UInt64
var macAddress: VZMACAddress
var display: VMDisplayConfig = VMDisplayConfig()

private(set) var nestedVirtualization: Bool

init(
platform: Platform,
cpuCountMin: Int,
Expand All @@ -65,31 +81,32 @@ struct VMConfig: Codable {
self.macAddress = macAddress
self.cpuCountMin = cpuCountMin
self.memorySizeMin = memorySizeMin
nestedVirtualization = false
cpuCount = cpuCountMin
memorySize = memorySizeMin
}

init(fromJSON: Data) throws {
self = try Config.jsonDecoder().decode(Self.self, from: fromJSON)
}

init(fromURL: URL) throws {
self = try Self(fromJSON: try Data(contentsOf: fromURL))
}

func toJSON() throws -> Data {
try Config.jsonEncoder().encode(self)
}

func save(toURL: URL) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try encoder.encode(self).write(to: toURL)
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

version = try container.decode(Int.self, forKey: .version)
os = try container.decodeIfPresent(OS.self, forKey: .os) ?? .darwin
arch = try container.decodeIfPresent(Architecture.self, forKey: .arch) ?? .arm64
Expand All @@ -110,7 +127,6 @@ struct VMConfig: Codable {
cpuCount = try container.decode(Int.self, forKey: .cpuCount)
memorySizeMin = try container.decode(UInt64.self, forKey: .memorySizeMin)
memorySize = try container.decode(UInt64.self, forKey: .memorySize)

let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
throw DecodingError.dataCorruptedError(
Expand All @@ -119,13 +135,14 @@ struct VMConfig: Codable {
debugDescription: "failed to initialize VZMacAddress using the provided value")
}
self.macAddress = macAddress

display = try container.decodeIfPresent(VMDisplayConfig.self, forKey: .display) ?? VMDisplayConfig()
nestedVirtualization = try container.decodeIfPresent(Bool.self, forKey: .nestedVirtualization) ?? false
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(version, forKey: .version)
try container.encode(os, forKey: .os)
try container.encode(arch, forKey: .arch)
Expand All @@ -136,33 +153,49 @@ struct VMConfig: Codable {
try container.encode(memorySize, forKey: .memorySize)
try container.encode(macAddress.string, forKey: .macAddress)
try container.encode(display, forKey: .display)
try container.encode(nestedVirtualization, forKey: .nestedVirtualization)
}

mutating func setCPU(cpuCount: Int) throws {
if os == .darwin && cpuCount < cpuCountMin {
throw LessThanMinimalResourcesError("VM should have \(cpuCountMin) CPU cores"
+ " at minimum (requested \(cpuCount))")
}

if cpuCount < VZVirtualMachineConfiguration.minimumAllowedCPUCount {
throw LessThanMinimalResourcesError("VM should have \(VZVirtualMachineConfiguration.minimumAllowedCPUCount) CPU cores"
+ " at minimum (requested \(cpuCount))")
}

self.cpuCount = cpuCount
}

mutating func setMemory(memorySize: UInt64) throws {
if os == .darwin && memorySize < memorySizeMin {
throw LessThanMinimalResourcesError("VM should have \(memorySizeMin) bytes"
+ " of memory at minimum (requested \(memorySize))")
}

if memorySize < VZVirtualMachineConfiguration.minimumAllowedMemorySize {
throw LessThanMinimalResourcesError("VM should have \(VZVirtualMachineConfiguration.minimumAllowedMemorySize) bytes"
+ " of memory at minimum (requested \(memorySize))")
}

self.memorySize = memorySize
}

mutating func enableNestedVirtualisation() throws {
if os == .darwin && nestedVirtualization {
throw UnsupportedNestedVirtualizationError("Nested virtualization is not supported for MacOS virtual machines")
}

if #available(macOS 15.0, *) {
if !VZGenericPlatformConfiguration.isNestedVirtualizationSupported {
throw UnsupportedNestedVirtualizationError("Nested virtualization is not supported on this hardware")
}
self.nestedVirtualization = true
} else {
throw UnsupportedNestedVirtualizationError("Nested virtualization requires macOS 15.0 or later")
}
}
}