mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
8fad8c4b9b
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>
141 lines
5.8 KiB
Swift
141 lines
5.8 KiB
Swift
// 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.")
|
|
}
|
|
}
|
|
}
|