tart run: support disabling disk synchronization for --disk (#872)

This commit is contained in:
Nikolay Edigaryev 2024-07-25 20:15:07 +04:00 committed by GitHub
parent 1b81b12760
commit 06cae1296e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 158 additions and 79 deletions

View File

@ -67,7 +67,7 @@ struct Run: AsyncParsableCommand {
var vncExperimental: Bool = false var vncExperimental: Bool = false
@Option(help: ArgumentHelp(""" @Option(help: ArgumentHelp("""
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\" --disk=\"/dev/disk0\" --disk "ghcr.io/cirruslabs/xcode:16.0:ro" --disk=\"nbd://localhost:10809/myDisk\") Additional disk attachments with an optional read-only and synchronization options (e.g. --disk="disk.bin", --disk="ubuntu.iso:ro", --disk="/dev/disk0", --disk "ghcr.io/cirruslabs/xcode:16.0:ro" or --disk="nbd://localhost:10809/myDisk:sync=none")
""", discussion: """ """, discussion: """
The disk attachment can be a: The disk attachment can be a:
@ -76,6 +76,12 @@ struct Run: AsyncParsableCommand {
* remote VM name whose disk will be mounted * remote VM name whose disk will be mounted
* Network Block Device (NBD) URL * Network Block Device (NBD) URL
Options are comma-separated and are as follows:
* ro attach the specified disk in read-only mode instead of the default read-write (e.g. --disk="disk.img:ro")
* sync=none disable data synchronization with the permanent storage to increase performance at the cost of a higher chance of data loss (e.g. --disk="disk.img:sync=none")
Learn how to create a disk image using Disk Utility here: https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac Learn how to create a disk image using Disk Utility here: https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
To work with block devices, the easiest way is to modify their permissions (e.g. by using "sudo chown $USER /dev/diskX") or to run the Tart binary as root, which affects locating Tart VMs. To work with block devices, the easiest way is to modify their permissions (e.g. by using "sudo chown $USER /dev/diskX") or to run the Tart binary as root, which affects locating Tart VMs.
@ -83,7 +89,7 @@ struct Run: AsyncParsableCommand {
To work around this pass TART_HOME explicitly: To work around this pass TART_HOME explicitly:
sudo TART_HOME="$HOME/.tart" tart run sonoma --disk=/dev/disk0 sudo TART_HOME="$HOME/.tart" tart run sonoma --disk=/dev/disk0
""", valueName: "path[:ro]")) """, valueName: "path[:options]"))
var disk: [String] = [] var disk: [String] = []
#if arch(arm64) #if arch(arm64)
@ -450,86 +456,11 @@ struct Run: AsyncParsableCommand {
func additionalDiskAttachments() throws -> [VZStorageDeviceConfiguration] { func additionalDiskAttachments() throws -> [VZStorageDeviceConfiguration] {
var result: [VZStorageDeviceConfiguration] = [] var result: [VZStorageDeviceConfiguration] = []
let readOnlySuffix = ":ro"
let expandedDiskPaths = disk.map { NSString(string:$0).expandingTildeInPath } let expandedDiskPaths = disk.map { NSString(string:$0).expandingTildeInPath }
for rawDisk in expandedDiskPaths { for rawDisk in expandedDiskPaths {
let diskReadOnly = rawDisk.hasSuffix(readOnlySuffix) result.append(try AdditionalDisk(parseFrom: rawDisk).configuration)
let diskPath = diskReadOnly ? String(rawDisk.prefix(rawDisk.count - readOnlySuffix.count)) : rawDisk
let diskURL = URL(string: diskPath)
if (["nbd", "nbds", "nbd+unix", "nbds+unix"].contains(diskURL?.scheme)) {
guard #available(macOS 14, *) else {
throw UnsupportedOSError("attaching Network Block Devices", "are")
}
let nbdAttachment = try VZNetworkBlockDeviceStorageDeviceAttachment(
url: diskURL!,
timeout: 30,
isForcedReadOnly: diskReadOnly,
synchronizationMode: VZDiskSynchronizationMode.none
)
result.append(VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment))
continue
}
let diskFileURL = URL(fileURLWithPath: diskPath)
if pathHasMode(diskPath, mode: S_IFBLK) {
guard #available(macOS 14, *) else {
throw UnsupportedOSError("attaching block devices", "are")
}
let fd = open(diskPath, diskReadOnly ? O_RDONLY : O_RDWR)
if fd == -1 {
let details = Errno(rawValue: CInt(errno))
switch details.rawValue {
case EBUSY:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "already in use, try umounting it via \"diskutil unmountDisk\" (when the whole disk) or \"diskutil umount\" (when mounting a single partition)")
case EACCES:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "permission denied, consider changing the disk's owner using \"sudo chown $USER \(diskFileURL.url.path)\" or run Tart as a superuser (see --disk help for more details on how to do that correctly)")
default:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "\(details)")
}
}
let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd, closeOnDealloc: true),
readOnly: diskReadOnly, synchronizationMode: .full)
result.append(VZVirtioBlockDeviceConfiguration(attachment: blockAttachment))
continue
}
// Support remote VM names in --disk command-line argument
if let remoteName = try? RemoteName(diskPath) {
let vmDir = try VMStorageOCI().open(remoteName)
// Unfortunately, VZDiskImageStorageDeviceAttachment does not support
// FileHandle, so we can't easily clone the disk, open it and unlink(2)
// to simplify the garbage collection, so use an intermediate directory.
let clonedDiskURL = try Config().tartTmpDir.appendingPathComponent("run-disk-\(UUID().uuidString)")
try FileManager.default.copyItem(at: vmDir.diskURL, to: clonedDiskURL)
let lock = try FileLock(lockURL: clonedDiskURL)
try lock.lock()
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: clonedDiskURL, readOnly: diskReadOnly)
result.append(VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment))
continue
}
// Error out if the disk is locked by the host (e.g. it was mounted in Finder),
// see https://github.com/cirruslabs/tart/issues/323 for more details.
if try !diskReadOnly && !FileLock(lockURL: diskFileURL).trylock() {
throw RuntimeError.DiskAlreadyInUse("disk \(diskFileURL.url.path) seems to be already in use, unmount it first in Finder")
}
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(
url: diskFileURL,
readOnly: diskReadOnly
)
result.append(VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment))
} }
return result return result
@ -752,6 +683,154 @@ struct VMView: NSViewRepresentable {
} }
} }
struct AdditionalDisk {
let configuration: VZStorageDeviceConfiguration
init(parseFrom: String) throws {
let (diskPath, readOnly, syncModeRaw) = Self.parseOptions(parseFrom)
self.configuration = try Self.craft(diskPath, readOnly: readOnly, syncModeRaw: syncModeRaw)
}
static func craft(_ diskPath: String, readOnly diskReadOnly: Bool, syncModeRaw: String) throws -> VZStorageDeviceConfiguration {
let diskURL = URL(string: diskPath)
if (["nbd", "nbds", "nbd+unix", "nbds+unix"].contains(diskURL?.scheme)) {
guard #available(macOS 14, *) else {
throw UnsupportedOSError("attaching Network Block Devices", "are")
}
let syncMode = try parseSyncMode(syncModeRaw)
let nbdAttachment = try VZNetworkBlockDeviceStorageDeviceAttachment(
url: diskURL!,
timeout: 30,
isForcedReadOnly: diskReadOnly,
synchronizationMode: syncMode
)
return VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment)
}
let diskFileURL = URL(fileURLWithPath: diskPath)
if pathHasMode(diskPath, mode: S_IFBLK) {
guard #available(macOS 14, *) else {
throw UnsupportedOSError("attaching block devices", "are")
}
let fd = open(diskPath, diskReadOnly ? O_RDONLY : O_RDWR)
if fd == -1 {
let details = Errno(rawValue: CInt(errno))
switch details.rawValue {
case EBUSY:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "already in use, try umounting it via \"diskutil unmountDisk\" (when the whole disk) or \"diskutil umount\" (when mounting a single partition)")
case EACCES:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "permission denied, consider changing the disk's owner using \"sudo chown $USER \(diskFileURL.url.path)\" or run Tart as a superuser (see --disk help for more details on how to do that correctly)")
default:
throw RuntimeError.FailedToOpenBlockDevice(diskFileURL.url.path, "\(details)")
}
}
let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd, closeOnDealloc: true),
readOnly: diskReadOnly, synchronizationMode: try parseSyncMode(syncModeRaw))
return VZVirtioBlockDeviceConfiguration(attachment: blockAttachment)
}
// Support remote VM names in --disk command-line argument
if let remoteName = try? RemoteName(diskPath) {
let vmDir = try VMStorageOCI().open(remoteName)
// Unfortunately, VZDiskImageStorageDeviceAttachment does not support
// FileHandle, so we can't easily clone the disk, open it and unlink(2)
// to simplify the garbage collection, so use an intermediate directory.
let clonedDiskURL = try Config().tartTmpDir.appendingPathComponent("run-disk-\(UUID().uuidString)")
try FileManager.default.copyItem(at: vmDir.diskURL, to: clonedDiskURL)
let lock = try FileLock(lockURL: clonedDiskURL)
try lock.lock()
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: clonedDiskURL, readOnly: diskReadOnly)
return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
}
// Error out if the disk is locked by the host (e.g. it was mounted in Finder),
// see https://github.com/cirruslabs/tart/issues/323 for more details.
if try !diskReadOnly && !FileLock(lockURL: diskFileURL).trylock() {
throw RuntimeError.DiskAlreadyInUse("disk \(diskFileURL.url.path) seems to be already in use, unmount it first in Finder")
}
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(
url: diskFileURL,
readOnly: diskReadOnly,
cachingMode: .automatic,
synchronizationMode: try Self.parseImageSyncMode(syncModeRaw)
)
return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
}
static func parseOptions(_ parseFrom: String) -> (String, Bool, String) {
var arguments = parseFrom.split(separator: ":")
let options = arguments.last!.split(separator: ",")
var readOnly: Bool = false
var syncModeRaw: String = ""
var foundAtLeastOneOption: Bool = false
for option in options {
switch true {
case option == "ro":
readOnly = true
foundAtLeastOneOption = true
case option.hasPrefix("sync="):
syncModeRaw = String(option.dropFirst("sync=".count))
foundAtLeastOneOption = true
default:
continue
}
}
if foundAtLeastOneOption {
arguments.removeLast()
}
return (arguments.joined(separator: ":"), readOnly, syncModeRaw)
}
@available(macOS 14, *)
static func parseSyncMode(_ parseFrom: String) throws -> VZDiskSynchronizationMode {
switch parseFrom {
case "none":
return .none
case "full":
return .full
case "":
return .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk synchronization mode: \"\(parseFrom)\"")
}
}
static func parseImageSyncMode(_ parseFrom: String) throws -> VZDiskImageSynchronizationMode {
switch parseFrom {
case "none":
return .none
case "full":
return .full
case "":
return .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk image synchronization mode: \"\(parseFrom)\"")
}
}
}
struct DirectoryShare { struct DirectoryShare {
let name: String? let name: String?
let path: URL let path: URL