321 lines
10 KiB
Swift
321 lines
10 KiB
Swift
//===----------------------------------------------------------------------===//
|
|||
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||
|
|
//
|
||
|
|
// 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.
|
||
|
|
//===----------------------------------------------------------------------===//
|
||
|
|
|
||
|
|
import ContainerizationError
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
#if os(macOS)
|
||
|
|
import Virtualization
|
||
|
|
#endif
|
||
|
|
|
||
|
|
/// A filesystem mount exposed to a container.
|
||
|
|
public struct Mount: Sendable {
|
||
|
|
/// The filesystem or mount type. This is the string
|
||
|
|
/// that will be used for the mount syscall itself.
|
||
|
|
public var type: String
|
||
|
|
/// The source path of the mount.
|
||
|
|
public var source: String
|
||
|
|
/// The destination path of the mount.
|
||
|
|
public var destination: String
|
||
|
|
/// Filesystem or mount specific options.
|
||
|
|
public var options: [String]
|
||
|
|
/// Runtime specific options. This can be used
|
||
|
|
/// as a way to discern what kind of device a vmm
|
||
|
|
/// should create for this specific mount (virtioblock
|
||
|
|
/// virtiofs etc.).
|
||
|
|
public let runtimeOptions: RuntimeOptions
|
||
|
|
|
||
|
|
/// A type representing a "hint" of what type
|
||
|
|
/// of mount this really is (block, directory, purely
|
||
|
|
/// guest mount) and a set of type specific options, if any.
|
||
|
|
public enum RuntimeOptions: Sendable {
|
||
|
|
case virtioblk([String])
|
||
|
|
case virtiofs([String])
|
||
|
|
case shared
|
||
|
|
case any([String])
|
||
|
|
}
|
||
|
|
|
||
|
|
public init(
|
||
|
|
type: String,
|
||
|
|
source: String,
|
||
|
|
destination: String,
|
||
|
|
options: [String],
|
||
|
|
runtimeOptions: RuntimeOptions
|
||
|
|
) {
|
||
|
|
self.type = type
|
||
|
|
self.source = source
|
||
|
|
self.destination = destination
|
||
|
|
self.options = options
|
||
|
|
self.runtimeOptions = runtimeOptions
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Mount representing a virtio block device.
|
||
|
|
public static func block(
|
||
|
|
format: String,
|
||
|
|
source: String,
|
||
|
|
destination: String,
|
||
|
|
options: [String] = [],
|
||
|
|
runtimeOptions: [String] = []
|
||
|
|
) -> Self {
|
||
|
|
.init(
|
||
|
|
type: format,
|
||
|
|
source: source,
|
||
|
|
destination: destination,
|
||
|
|
options: options,
|
||
|
|
runtimeOptions: .virtioblk(runtimeOptions)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Mount representing a virtiofs share.
|
||
|
|
public static func share(
|
||
|
|
source: String,
|
||
|
|
destination: String,
|
||
|
|
options: [String] = [],
|
||
|
|
runtimeOptions: [String] = []
|
||
|
|
) -> Self {
|
||
|
|
.init(
|
||
|
|
type: "virtiofs",
|
||
|
|
source: source,
|
||
|
|
destination: destination,
|
||
|
|
options: options,
|
||
|
|
runtimeOptions: .virtiofs(runtimeOptions)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A generic mount.
|
||
|
|
public static func any(
|
||
|
|
type: String,
|
||
|
|
source: String,
|
||
|
|
destination: String,
|
||
|
|
options: [String] = [],
|
||
|
|
runtimeOptions: [String] = []
|
||
|
|
) -> Self {
|
||
|
|
.init(
|
||
|
|
type: type,
|
||
|
|
source: source,
|
||
|
|
destination: destination,
|
||
|
|
options: options,
|
||
|
|
runtimeOptions: .any(runtimeOptions)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A mount referencing a shared pod volume by name.
|
||
|
|
public static func sharedMount(
|
||
|
|
name: String,
|
||
|
|
destination: String,
|
||
|
|
options: [String] = []
|
||
|
|
) -> Self {
|
||
|
|
.init(
|
||
|
|
type: "none",
|
||
|
|
source: name,
|
||
|
|
destination: destination,
|
||
|
|
options: options,
|
||
|
|
runtimeOptions: .shared
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
#if os(macOS)
|
||
|
|
/// Clone the Mount to the provided path.
|
||
|
|
///
|
||
|
|
/// This uses `clonefile` to provide a copy-on-write copy of the Mount.
|
||
|
|
public func clone(to: String) throws -> Self {
|
||
|
|
let fm = FileManager.default
|
||
|
|
let src = self.source
|
||
|
|
try fm.copyItem(atPath: src, toPath: to)
|
||
|
|
|
||
|
|
return .init(
|
||
|
|
type: self.type,
|
||
|
|
source: to,
|
||
|
|
destination: self.destination,
|
||
|
|
options: self.options,
|
||
|
|
runtimeOptions: self.runtimeOptions
|
||
|
|
)
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
#if os(macOS)
|
||
|
|
|
||
|
|
extension Mount {
|
||
|
|
private enum StorageAttachmentType {
|
||
|
|
case diskImage
|
||
|
|
case networkBlockDevice
|
||
|
|
}
|
||
|
|
|
||
|
|
private var storageAttachmentType: StorageAttachmentType {
|
||
|
|
let nbdSchemes = ["nbd://", "nbds://", "nbd+unix://", "nbds+unix://"]
|
||
|
|
if nbdSchemes.contains(where: { self.source.hasPrefix($0) }) {
|
||
|
|
return .networkBlockDevice
|
||
|
|
}
|
||
|
|
return .diskImage
|
||
|
|
}
|
||
|
|
|
||
|
|
func configure(config: inout VZVirtualMachineConfiguration) throws {
|
||
|
|
switch self.runtimeOptions {
|
||
|
|
case .virtioblk(let options):
|
||
|
|
let device: VZStorageDeviceAttachment
|
||
|
|
switch self.storageAttachmentType {
|
||
|
|
case .networkBlockDevice:
|
||
|
|
device = try VZNetworkBlockDeviceStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
|
||
|
|
case .diskImage:
|
||
|
|
device = try VZDiskImageStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
|
||
|
|
}
|
||
|
|
let attachment = VZVirtioBlockDeviceConfiguration(attachment: device)
|
||
|
|
config.storageDevices.append(attachment)
|
||
|
|
case .virtiofs(_):
|
||
|
|
// VirtioFS mounts are handled centrally via VZMultipleDirectoryShare in VZVirtualMachineInstance
|
||
|
|
// No per-mount device configuration needed
|
||
|
|
break
|
||
|
|
case .shared, .any:
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
extension VZDiskImageStorageDeviceAttachment {
|
||
|
|
static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZDiskImageStorageDeviceAttachment {
|
||
|
|
var synchronizationMode: VZDiskImageSynchronizationMode = .fsync
|
||
|
|
var cachingMode: VZDiskImageCachingMode = .cached
|
||
|
|
|
||
|
|
for option in options {
|
||
|
|
let split = option.split(separator: "=")
|
||
|
|
if split.count != 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
let key = String(split[0])
|
||
|
|
let value = String(split[1])
|
||
|
|
|
||
|
|
switch key {
|
||
|
|
case "vzDiskImageCachingMode":
|
||
|
|
switch value {
|
||
|
|
case "automatic":
|
||
|
|
cachingMode = .automatic
|
||
|
|
case "cached":
|
||
|
|
cachingMode = .cached
|
||
|
|
case "uncached":
|
||
|
|
cachingMode = .uncached
|
||
|
|
default:
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "unknown vzDiskImageCachingMode value for virtio block device: \(value)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
case "vzDiskImageSynchronizationMode":
|
||
|
|
switch value {
|
||
|
|
case "full":
|
||
|
|
synchronizationMode = .full
|
||
|
|
case "fsync":
|
||
|
|
synchronizationMode = .fsync
|
||
|
|
case "none":
|
||
|
|
synchronizationMode = .none
|
||
|
|
default:
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "unknown vzDiskImageSynchronizationMode value for virtio block device: \(value)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "unknown vmm option encountered: \(key)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return try VZDiskImageStorageDeviceAttachment(
|
||
|
|
url: URL(filePath: mount.source),
|
||
|
|
readOnly: mount.readonly,
|
||
|
|
cachingMode: cachingMode,
|
||
|
|
synchronizationMode: synchronizationMode
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
extension VZNetworkBlockDeviceStorageDeviceAttachment {
|
||
|
|
static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZNetworkBlockDeviceStorageDeviceAttachment {
|
||
|
|
guard let url = URL(string: mount.source) else {
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "invalid NBD URL: \(mount.source)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
var timeout: TimeInterval = 5
|
||
|
|
var synchronizationMode: VZDiskSynchronizationMode = .full
|
||
|
|
|
||
|
|
for option in options {
|
||
|
|
let split = option.split(separator: "=")
|
||
|
|
if split.count != 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
let key = String(split[0])
|
||
|
|
let value = String(split[1])
|
||
|
|
|
||
|
|
switch key {
|
||
|
|
case "vzTimeout":
|
||
|
|
guard let t = TimeInterval(value) else {
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "invalid vzTimeout value for NBD device: \(value)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
timeout = t
|
||
|
|
case "vzSynchronizationMode":
|
||
|
|
switch value {
|
||
|
|
case "full":
|
||
|
|
synchronizationMode = .full
|
||
|
|
case "none":
|
||
|
|
synchronizationMode = .none
|
||
|
|
default:
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "unknown vzSynchronizationMode value for NBD device: \(value)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
throw ContainerizationError(
|
||
|
|
.invalidArgument,
|
||
|
|
message: "unknown vmm option encountered: \(key)"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return try VZNetworkBlockDeviceStorageDeviceAttachment(
|
||
|
|
url: url,
|
||
|
|
timeout: timeout,
|
||
|
|
isForcedReadOnly: mount.readonly,
|
||
|
|
synchronizationMode: synchronizationMode
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif
|
||
|
|
|
||
|
|
extension Mount {
|
||
|
|
fileprivate var readonly: Bool {
|
||
|
|
self.options.contains("ro")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns true if this mount is a virtio block device.
|
||
|
|
public var isBlock: Bool {
|
||
|
|
if case .virtioblk = self.runtimeOptions {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|