mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-26 05:09:41 +00:00
tstest/tailmac: add customized macOS virtualization tooling (#13146)
updates tailcale/corp#22371 Adds custom macOS vm tooling. See the README for the general gist, but this will spin up VMs with unixgram capable network interfaces listening to a named socket, and with a virtio socket device for host-guest communication. We can add other devices like consoles, serial, etc as needed. The whole things is buildable with a single make command, and everything is controllable via the command line using the TailMac utility. This should all be generally functional but takes a few shortcuts with error handling and the like. The virtio socket device support has not been tested and may require some refinement. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
140
tstest/tailmac/Swift/TailMac/VMInstaller.swift
Normal file
140
tstest/tailmac/Swift/TailMac/VMInstaller.swift
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
class VMInstaller: NSObject {
|
||||
private var installationObserver: NSKeyValueObservation?
|
||||
private var virtualMachine: VZVirtualMachine!
|
||||
|
||||
private var config: Config
|
||||
private var helper: TailMacConfigHelper
|
||||
|
||||
init(_ config: Config) {
|
||||
self.config = config
|
||||
helper = TailMacConfigHelper(config: config)
|
||||
}
|
||||
|
||||
public func installMacOS(ipswURL: URL) {
|
||||
print("Attempting to install from IPSW at \(ipswURL).")
|
||||
VZMacOSRestoreImage.load(from: ipswURL, completionHandler: { [self](result: Result<VZMacOSRestoreImage, Error>) in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
fatalError(error.localizedDescription)
|
||||
|
||||
case let .success(restoreImage):
|
||||
installMacOS(restoreImage: restoreImage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Internal helper functions.
|
||||
|
||||
private func installMacOS(restoreImage: VZMacOSRestoreImage) {
|
||||
guard let macOSConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else {
|
||||
fatalError("No supported configuration available.")
|
||||
}
|
||||
|
||||
if !macOSConfiguration.hardwareModel.isSupported {
|
||||
fatalError("macOSConfiguration configuration isn't supported on the current host.")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
setupVirtualMachine(macOSConfiguration: macOSConfiguration)
|
||||
startInstallation(restoreImageURL: restoreImage.url)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Create the Mac platform configuration.
|
||||
|
||||
private func createMacPlatformConfiguration(macOSConfiguration: VZMacOSConfigurationRequirements) -> VZMacPlatformConfiguration {
|
||||
let macPlatformConfiguration = VZMacPlatformConfiguration()
|
||||
|
||||
|
||||
let auxiliaryStorage: VZMacAuxiliaryStorage
|
||||
do {
|
||||
auxiliaryStorage = try VZMacAuxiliaryStorage(creatingStorageAt: config.auxiliaryStorageURL,
|
||||
hardwareModel: macOSConfiguration.hardwareModel,
|
||||
options: [])
|
||||
} catch {
|
||||
fatalError("Unable to create aux storage at \(config.auxiliaryStorageURL) \(error)")
|
||||
}
|
||||
macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage
|
||||
macPlatformConfiguration.hardwareModel = macOSConfiguration.hardwareModel
|
||||
macPlatformConfiguration.machineIdentifier = VZMacMachineIdentifier()
|
||||
|
||||
// Store the hardware model and machine identifier to disk so that you
|
||||
// can retrieve them for subsequent boots.
|
||||
try! macPlatformConfiguration.hardwareModel.dataRepresentation.write(to: config.hardwareModelURL)
|
||||
try! macPlatformConfiguration.machineIdentifier.dataRepresentation.write(to: config.machineIdentifierURL)
|
||||
|
||||
return macPlatformConfiguration
|
||||
}
|
||||
|
||||
private func setupVirtualMachine(macOSConfiguration: VZMacOSConfigurationRequirements) {
|
||||
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
|
||||
|
||||
virtualMachineConfiguration.platform = createMacPlatformConfiguration(macOSConfiguration: macOSConfiguration)
|
||||
virtualMachineConfiguration.cpuCount = helper.computeCPUCount()
|
||||
if virtualMachineConfiguration.cpuCount < macOSConfiguration.minimumSupportedCPUCount {
|
||||
fatalError("CPUCount isn't supported by the macOS configuration.")
|
||||
}
|
||||
|
||||
virtualMachineConfiguration.memorySize = helper.computeMemorySize()
|
||||
if virtualMachineConfiguration.memorySize < macOSConfiguration.minimumSupportedMemorySize {
|
||||
fatalError("memorySize isn't supported by the macOS configuration.")
|
||||
}
|
||||
|
||||
createDiskImage()
|
||||
|
||||
virtualMachineConfiguration.bootLoader = helper.createBootLoader()
|
||||
virtualMachineConfiguration.graphicsDevices = [helper.createGraphicsDeviceConfiguration()]
|
||||
virtualMachineConfiguration.storageDevices = [helper.createBlockDeviceConfiguration()]
|
||||
virtualMachineConfiguration.networkDevices = [helper.createNetworkDeviceConfiguration(), helper.createSocketNetworkDeviceConfiguration()]
|
||||
virtualMachineConfiguration.pointingDevices = [helper.createPointingDeviceConfiguration()]
|
||||
virtualMachineConfiguration.keyboards = [helper.createKeyboardConfiguration()]
|
||||
|
||||
try! virtualMachineConfiguration.validate()
|
||||
try! virtualMachineConfiguration.validateSaveRestoreSupport()
|
||||
|
||||
virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
|
||||
}
|
||||
|
||||
private func startInstallation(restoreImageURL: URL) {
|
||||
let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: restoreImageURL)
|
||||
|
||||
print("Starting installation.")
|
||||
installer.install(completionHandler: { (result: Result<Void, Error>) in
|
||||
if case let .failure(error) = result {
|
||||
fatalError(error.localizedDescription)
|
||||
} else {
|
||||
print("Installation succeeded.")
|
||||
}
|
||||
})
|
||||
|
||||
// Observe installation progress.
|
||||
installationObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in
|
||||
print("Installation progress: \(change.newValue! * 100).")
|
||||
}
|
||||
}
|
||||
|
||||
// Create an empty disk image for the virtual machine.
|
||||
private func createDiskImage() {
|
||||
let diskFd = open(config.diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)
|
||||
if diskFd == -1 {
|
||||
fatalError("Cannot create disk image.")
|
||||
}
|
||||
|
||||
// 72 GB disk space.
|
||||
var result = ftruncate(diskFd, config.diskSize)
|
||||
if result != 0 {
|
||||
fatalError("ftruncate() failed.")
|
||||
}
|
||||
|
||||
result = close(diskFd)
|
||||
if result != 0 {
|
||||
fatalError("Failed to close the disk image.")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user