chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
import Logging
|
||||
import NIOCore
|
||||
import NIOHTTP1
|
||||
import NIOPosix
|
||||
|
||||
extension CloudHypervisor {
|
||||
/// A high-level client for Cloud Hypervisor's REST API over a Unix Domain Socket.
|
||||
///
|
||||
/// Use ``init(socketPath:eventLoopGroup:logger:)`` to construct a client, then
|
||||
/// call endpoint-specific methods (added as extensions in `Endpoints/`).
|
||||
///
|
||||
/// The internal `get(_:)` / `put(_:)` / `put(_:body:)` helpers are used by
|
||||
/// endpoint extensions in A8-A10 and are intentionally not public.
|
||||
public final class Client: Sendable {
|
||||
private let http: HTTPOverUDSClient
|
||||
private let group: any EventLoopGroup
|
||||
private let ownsGroup: Bool
|
||||
private let encoder: JSONEncoder
|
||||
private let decoder: JSONDecoder
|
||||
|
||||
/// Create a client that communicates with Cloud Hypervisor over the given socket.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - socketPath: A `file://` URL whose `.path` points to the socket.
|
||||
/// - eventLoopGroup: The NIO event loop group to use. When `nil` the client
|
||||
/// creates and owns its own group. Callers wanting deterministic
|
||||
/// resource release should pass a group they manage and call
|
||||
/// ``shutdown()`` themselves; the deinit fallback shuts down
|
||||
/// asynchronously and may outlive the `Client` instance briefly.
|
||||
/// - logger: Logger for transport-level diagnostics.
|
||||
/// - requestTimeout: Per-request deadline. A request that does not
|
||||
/// complete within this window fails with
|
||||
/// ``CloudHypervisor/Error/transport(_:)``. Defaults to 30 seconds.
|
||||
/// - Throws: ``CloudHypervisor/Error/invalidSocketPath(_:)`` when `socketPath`
|
||||
/// is not a `file://` URL.
|
||||
public init(
|
||||
socketPath: URL,
|
||||
eventLoopGroup: (any EventLoopGroup)? = nil,
|
||||
logger: Logger = Logger(label: "CloudHypervisor.Client"),
|
||||
requestTimeout: TimeAmount = .seconds(30)
|
||||
) throws {
|
||||
guard socketPath.isFileURL else {
|
||||
throw CloudHypervisor.Error.invalidSocketPath(socketPath.absoluteString)
|
||||
}
|
||||
if let eventLoopGroup {
|
||||
self.ownsGroup = false
|
||||
self.group = eventLoopGroup
|
||||
} else {
|
||||
self.ownsGroup = true
|
||||
self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
||||
}
|
||||
self.http = HTTPOverUDSClient(
|
||||
socketPath: socketPath.path,
|
||||
group: self.group,
|
||||
logger: logger,
|
||||
requestTimeout: requestTimeout
|
||||
)
|
||||
self.encoder = JSONEncoder()
|
||||
self.decoder = JSONDecoder()
|
||||
}
|
||||
|
||||
/// Drain the underlying `AsyncHTTPClient`, and shut down the NIO
|
||||
/// event-loop group when this client owns it. Idempotent. Prefer
|
||||
/// calling this explicitly over relying on the deinit fallback —
|
||||
/// `shutdown()` waits for in-flight I/O to drain.
|
||||
///
|
||||
/// Callers that pass in a shared `eventLoopGroup` MUST call this
|
||||
/// before tearing down that group. AsyncHTTPClient parks deferred
|
||||
/// connection-close work on the group's event loops after each
|
||||
/// response returns; shutting the group down before that work
|
||||
/// runs trips NIO's "Cannot schedule tasks on an EventLoop that
|
||||
/// has already shut down" warning (and a forced crash in future
|
||||
/// NIO releases).
|
||||
public func shutdown() async throws {
|
||||
try await http.shutdown()
|
||||
if ownsGroup {
|
||||
try await group.shutdownGracefully()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Use the async-dispatched shutdown rather than
|
||||
// `syncShutdownGracefully()`. The sync variant blocks the calling
|
||||
// thread until every event loop drains, which deadlocks if deinit
|
||||
// happens to run on one of the group's event loop threads (e.g.
|
||||
// the last release came from inside a NIO callback). The
|
||||
// callback-based variant schedules shutdown on its own queue and
|
||||
// returns immediately — at the cost of giving up any signal that
|
||||
// shutdown completed. Callers who need that signal should call
|
||||
// `shutdown()` explicitly before letting the client deinit.
|
||||
if ownsGroup {
|
||||
group.shutdownGracefully(queue: .global()) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal request dispatch helpers
|
||||
//
|
||||
// Endpoint extensions (A8/A9/A10) call these to build their public API.
|
||||
// They are internal (not public) because all public surface lives in those
|
||||
// extensions.
|
||||
|
||||
/// GET `path`, decode the response body as `Response`.
|
||||
func get<Response: Decodable & Sendable>(_ path: String) async throws -> Response {
|
||||
try await sendAndDecode(method: .GET, path: path, body: nil)
|
||||
}
|
||||
|
||||
/// PUT `path` with no body, discard the response.
|
||||
func put(_ path: String) async throws {
|
||||
try await sendVoid(method: .PUT, path: path, body: nil)
|
||||
}
|
||||
|
||||
/// PUT `path` with a JSON-encoded body, discard the response.
|
||||
func put<Body: Encodable & Sendable>(_ path: String, body: Body) async throws {
|
||||
let data = try encoder.encode(body)
|
||||
try await sendVoid(method: .PUT, path: path, body: data)
|
||||
}
|
||||
|
||||
/// PUT `path` with a JSON-encoded body, decode the response as `Response`.
|
||||
func put<Body: Encodable & Sendable, Response: Decodable & Sendable>(
|
||||
_ path: String,
|
||||
body: Body
|
||||
) async throws -> Response {
|
||||
let data = try encoder.encode(body)
|
||||
return try await sendAndDecode(method: .PUT, path: path, body: data)
|
||||
}
|
||||
|
||||
// MARK: - Private machinery
|
||||
|
||||
private func sendAndDecode<Response: Decodable & Sendable>(
|
||||
method: HTTPMethod,
|
||||
path: String,
|
||||
body: Data?
|
||||
) async throws -> Response {
|
||||
let resp = try await http.send(method: method, uri: path, body: body)
|
||||
guard (200..<300).contains(Int(resp.status.code)) else {
|
||||
throw CloudHypervisor.Error.http(status: resp.status, body: resp.body)
|
||||
}
|
||||
do {
|
||||
return try decoder.decode(Response.self, from: resp.body)
|
||||
} catch {
|
||||
throw CloudHypervisor.Error.decoding(error, body: resp.body)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendVoid(method: HTTPMethod, path: String, body: Data?) async throws {
|
||||
let resp = try await http.send(method: method, uri: path, body: body)
|
||||
guard (200..<300).contains(Int(resp.status.code)) else {
|
||||
throw CloudHypervisor.Error.http(status: resp.status, body: resp.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
import NIOHTTP1
|
||||
|
||||
extension CloudHypervisor {
|
||||
public enum Error: Swift.Error, Sendable {
|
||||
case transport(any Swift.Error)
|
||||
case http(status: HTTPResponseStatus, body: Data)
|
||||
case decoding(any Swift.Error, body: Data)
|
||||
case invalidSocketPath(String)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public enum CloudHypervisor {}
|
||||
@@ -0,0 +1,55 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
|
||||
extension CloudHypervisor.Client {
|
||||
/// Hotplug a virtio-blk disk device into a running VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.add-disk` in the Cloud Hypervisor REST API.
|
||||
public func vmAddDisk(_ config: CloudHypervisor.DiskConfig) async throws -> CloudHypervisor.PciDeviceInfo {
|
||||
try await put("/api/v1/vm.add-disk", body: config)
|
||||
}
|
||||
|
||||
/// Hotplug a virtio-fs filesystem device into a running VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.add-fs` in the Cloud Hypervisor REST API.
|
||||
public func vmAddFs(_ config: CloudHypervisor.FsConfig) async throws -> CloudHypervisor.PciDeviceInfo {
|
||||
try await put("/api/v1/vm.add-fs", body: config)
|
||||
}
|
||||
|
||||
/// Hotplug a virtio-net network device into a running VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.add-net` in the Cloud Hypervisor REST API.
|
||||
public func vmAddNet(_ config: CloudHypervisor.NetConfig) async throws -> CloudHypervisor.PciDeviceInfo {
|
||||
try await put("/api/v1/vm.add-net", body: config)
|
||||
}
|
||||
|
||||
/// Hotplug a virtio-vsock device into a running VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.add-vsock` in the Cloud Hypervisor REST API.
|
||||
public func vmAddVsock(_ config: CloudHypervisor.VsockConfig) async throws -> CloudHypervisor.PciDeviceInfo {
|
||||
try await put("/api/v1/vm.add-vsock", body: config)
|
||||
}
|
||||
|
||||
/// Remove a hotplugged device from a running VM by its identifier.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.remove-device` in the Cloud Hypervisor REST API.
|
||||
public func vmRemoveDevice(id: String) async throws {
|
||||
struct Request: Encodable, Sendable { let id: String }
|
||||
try await put("/api/v1/vm.remove-device", body: Request(id: id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
|
||||
extension CloudHypervisor.Client {
|
||||
/// Create a VM with the given configuration.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.create` in the Cloud Hypervisor REST API.
|
||||
public func vmCreate(_ config: CloudHypervisor.VmConfig) async throws {
|
||||
try await put("/api/v1/vm.create", body: config)
|
||||
}
|
||||
|
||||
/// Boot the VM (transition from Created → Running).
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.boot` in the Cloud Hypervisor REST API.
|
||||
public func vmBoot() async throws {
|
||||
try await put("/api/v1/vm.boot")
|
||||
}
|
||||
|
||||
/// Shut down the VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.shutdown` in the Cloud Hypervisor REST API.
|
||||
public func vmShutdown() async throws {
|
||||
try await put("/api/v1/vm.shutdown")
|
||||
}
|
||||
|
||||
/// Retrieve runtime information about the VM.
|
||||
///
|
||||
/// Maps to `GET /api/v1/vm.info` in the Cloud Hypervisor REST API.
|
||||
public func vmInfo() async throws -> CloudHypervisor.VmInfo {
|
||||
try await get("/api/v1/vm.info")
|
||||
}
|
||||
|
||||
/// Pause the running VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.pause` in the Cloud Hypervisor REST API.
|
||||
public func vmPause() async throws {
|
||||
try await put("/api/v1/vm.pause")
|
||||
}
|
||||
|
||||
/// Resume a paused VM.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vm.resume` in the Cloud Hypervisor REST API.
|
||||
public func vmResume() async throws {
|
||||
try await put("/api/v1/vm.resume")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
|
||||
extension CloudHypervisor.Client {
|
||||
/// Ping the Cloud Hypervisor VMM process and return its version information.
|
||||
///
|
||||
/// Maps to `GET /api/v1/vmm.ping` in the Cloud Hypervisor REST API.
|
||||
public func vmmPing() async throws -> CloudHypervisor.VmmPingResponse {
|
||||
try await get("/api/v1/vmm.ping")
|
||||
}
|
||||
|
||||
/// Request the Cloud Hypervisor VMM process to shut down gracefully.
|
||||
///
|
||||
/// Maps to `PUT /api/v1/vmm.shutdown` in the Cloud Hypervisor REST API.
|
||||
public func vmmShutdown() async throws {
|
||||
try await put("/api/v1/vmm.shutdown")
|
||||
}
|
||||
|
||||
/// Retrieve information about the Cloud Hypervisor VMM process.
|
||||
///
|
||||
/// Maps to `GET /api/v1/vmm.info` in the Cloud Hypervisor REST API.
|
||||
public func vmmInfo() async throws -> CloudHypervisor.VmmInfo {
|
||||
try await get("/api/v1/vmm.info")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 AsyncHTTPClient
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIOConcurrencyHelpers
|
||||
import NIOCore
|
||||
import NIOHTTP1
|
||||
|
||||
// MARK: - HTTPResponse
|
||||
|
||||
/// An HTTP response received from Cloud Hypervisor's REST API.
|
||||
struct HTTPResponse: Sendable {
|
||||
let status: HTTPResponseStatus
|
||||
let headers: HTTPHeaders
|
||||
let body: Data
|
||||
}
|
||||
|
||||
// MARK: - HTTPOverUDSClient
|
||||
|
||||
/// A minimal HTTP/1.1 client that speaks over a Unix Domain Socket. Backed
|
||||
/// by `AsyncHTTPClient` so connection lifecycle, timeout handling, and the
|
||||
/// head/body/end write race we used to manage manually all live in the
|
||||
/// library rather than in this file.
|
||||
///
|
||||
/// AHC selects UDS via the `http+unix://` URL scheme (the supplied
|
||||
/// `URL(httpURLWithSocketPath:uri:)` initializer does the percent-encoding).
|
||||
/// Each `HTTPOverUDSClient` owns a fresh `HTTPClient` configured with
|
||||
/// `eventLoopGroupProvider: .shared(group)` so the underlying NIO group is
|
||||
/// the caller's to shut down — `httpClient.shutdown` only releases the
|
||||
/// client's own state.
|
||||
final class HTTPOverUDSClient: Sendable {
|
||||
private let socketPath: String
|
||||
private let httpClient: HTTPClient
|
||||
private let logger: Logger
|
||||
private let requestTimeout: TimeAmount
|
||||
// One-shot flag tracking whether shutdown has been initiated, so
|
||||
// explicit `shutdown()` is idempotent and `deinit` skips its fallback
|
||||
// when an explicit shutdown already drained the HTTPClient.
|
||||
private let didShutdown: NIOLockedValueBox<Bool>
|
||||
|
||||
init(
|
||||
socketPath: String,
|
||||
group: any EventLoopGroup,
|
||||
logger: Logger,
|
||||
requestTimeout: TimeAmount = .seconds(30)
|
||||
) {
|
||||
self.socketPath = socketPath
|
||||
self.httpClient = HTTPClient(
|
||||
eventLoopGroupProvider: .shared(group),
|
||||
configuration: .init()
|
||||
)
|
||||
self.logger = logger
|
||||
self.requestTimeout = requestTimeout
|
||||
self.didShutdown = NIOLockedValueBox(false)
|
||||
}
|
||||
|
||||
/// Drain the underlying HTTPClient and wait for in-flight I/O to
|
||||
/// finish. Idempotent — safe to call multiple times.
|
||||
///
|
||||
/// MUST be called before the shared event-loop group is torn down.
|
||||
/// AsyncHTTPClient leaves deferred connection-cleanup work parked on
|
||||
/// the group's event loops after a response returns; if the group is
|
||||
/// shut down first, that deferred work fails to schedule and SwiftNIO
|
||||
/// prints "Cannot schedule tasks on an EventLoop that has already
|
||||
/// shut down" (and will upgrade to a forced crash in future NIO
|
||||
/// releases).
|
||||
func shutdown() async throws {
|
||||
let already = didShutdown.withLockedValue { state -> Bool in
|
||||
if state { return true }
|
||||
state = true
|
||||
return false
|
||||
}
|
||||
if already { return }
|
||||
try await httpClient.shutdown()
|
||||
}
|
||||
|
||||
/// Send an HTTP request and return the response.
|
||||
///
|
||||
/// Translates AHC errors → ``CloudHypervisor/Error/transport(_:)`` so
|
||||
/// callers see a uniform error type regardless of failure mode.
|
||||
func send(
|
||||
method: HTTPMethod,
|
||||
uri: String,
|
||||
body: Data?,
|
||||
headers: HTTPHeaders = [:]
|
||||
) async throws -> HTTPResponse {
|
||||
// AHC handles the percent-encoding. nil only on a path that can't
|
||||
// be encoded — surface it the same way the public Client init does.
|
||||
guard let url = URL(httpURLWithSocketPath: socketPath, uri: uri) else {
|
||||
throw CloudHypervisor.Error.invalidSocketPath(socketPath)
|
||||
}
|
||||
|
||||
var request = HTTPClientRequest(url: url.absoluteString)
|
||||
request.method = method
|
||||
|
||||
// Preserve all caller-supplied headers verbatim.
|
||||
for (name, value) in headers {
|
||||
request.headers.replaceOrAdd(name: name, value: value)
|
||||
}
|
||||
|
||||
// `Connection: close` is preserved from the previous transport. CH
|
||||
// accepts both close and keep-alive, but close is the safer default
|
||||
// until we have explicit smoke coverage of long-lived per-VM
|
||||
// keep-alive behavior. Each request goes to a different per-VM UDS
|
||||
// anyway so there's nothing to pool.
|
||||
request.headers.replaceOrAdd(name: "Connection", value: "close")
|
||||
|
||||
// Body framing. CH's HTTP parser rejects body-less PUTs unless the
|
||||
// request carries `Content-Length: 0` instead of falling back to
|
||||
// chunked transfer encoding.
|
||||
//
|
||||
// How AHC actually frames the request is subtle:
|
||||
// `RequestValidation.setTransportFraming` strips any manually-set
|
||||
// `Content-Length` and re-derives framing from the body's known
|
||||
// length. Assigning `.bytes(ByteBuffer())` (rather than leaving
|
||||
// body nil) sets `bodyLength == .known(0)`, which AHC then frames
|
||||
// as `Content-Length: 0` for PUT/POST per RFC 7230 §3.3.2. Leaving
|
||||
// body nil would surface as `bodyLength == .unknown`, and AHC may
|
||||
// emit chunked framing or no framing at all, which CH rejects.
|
||||
// The explicit `Content-Length: 0` header set below is documentation
|
||||
// of intent — AHC removes it before deriving framing — but the
|
||||
// wire shape is determined by the empty body assignment.
|
||||
//
|
||||
// Regression test: ClientTests.bodylessPUTSendsContentLengthZero.
|
||||
if let body, !body.isEmpty {
|
||||
if request.headers["Content-Type"].isEmpty {
|
||||
request.headers.add(name: "Content-Type", value: "application/json")
|
||||
}
|
||||
request.body = .bytes(ByteBuffer(bytes: body))
|
||||
} else {
|
||||
request.headers.replaceOrAdd(name: "Content-Length", value: "0")
|
||||
request.body = .bytes(ByteBuffer())
|
||||
}
|
||||
|
||||
let deadline = NIODeadline.now() + requestTimeout
|
||||
logger.debug("HTTPOverUDSClient: \(method) \(uri) → \(socketPath)")
|
||||
|
||||
do {
|
||||
let response = try await httpClient.execute(
|
||||
request,
|
||||
deadline: deadline,
|
||||
logger: logger
|
||||
)
|
||||
|
||||
// 16 MiB is far larger than any CH response we expect — vm.info,
|
||||
// the largest, measures in low-KB even for many-disk VMs. The
|
||||
// cap exists so a wedged server can't OOM us.
|
||||
//
|
||||
// Use `readableBytesView` + the Sequence-based Data init rather
|
||||
// than `Data(buffer: ByteBuffer)`: the latter requires
|
||||
// `NIOFoundationCompat`, which the Linux musl build doesn't
|
||||
// pull in via Foundation by default.
|
||||
let bodyBuffer = try await response.body.collect(upTo: 1 << 24)
|
||||
let bodyData = Data(bodyBuffer.readableBytesView)
|
||||
|
||||
logger.debug("HTTPOverUDSClient: \(method) \(uri) ← \(response.status.code)")
|
||||
return HTTPResponse(
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body: bodyData
|
||||
)
|
||||
} catch let error as CloudHypervisor.Error {
|
||||
throw error
|
||||
} catch {
|
||||
throw CloudHypervisor.Error.transport(error)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Fire the callback-based shutdown only when `shutdown()` wasn't
|
||||
// already called. The sync variant would deadlock if deinit
|
||||
// happened to run on one of the HTTPClient's own event loops
|
||||
// (commit fe1c95cf); the callback variant returns immediately at
|
||||
// the cost of any completion signal. If explicit shutdown
|
||||
// already ran, the HTTPClient is drained and a second call would
|
||||
// just return `alreadyShutdown` — but it can still try to
|
||||
// schedule the callback on the (now-dead) event loop, which is
|
||||
// exactly the failure mode this whole flag guards against.
|
||||
let already = didShutdown.withLockedValue { state -> Bool in
|
||||
if state { return true }
|
||||
state = true
|
||||
return false
|
||||
}
|
||||
guard !already else { return }
|
||||
httpClient.shutdown { _ in }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# CloudHypervisor
|
||||
|
||||
A standalone Swift library for driving the [cloud-hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) REST API over a Unix domain socket. The package compiles on both macOS and Linux, though `cloud-hypervisor` itself only runs on Linux.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [swift-nio](https://github.com/apple/swift-nio): `NIOCore`, `NIOPosix`, `NIOHTTP1`, `NIOConcurrencyHelpers`
|
||||
- [swift-log](https://github.com/apple/swift-log): `Logging`
|
||||
|
||||
There are no transitive dependencies on any other `containerization` library types.
|
||||
|
||||
## Usage
|
||||
|
||||
```swift
|
||||
import CloudHypervisor
|
||||
|
||||
let client = try CloudHypervisor.Client(
|
||||
socketPath: URL(filePath: "/tmp/ch-foo/api.sock")
|
||||
)
|
||||
|
||||
try await client.vmmPing()
|
||||
try await client.vmCreate(VmConfig(/* ... */))
|
||||
try await client.vmBoot()
|
||||
```
|
||||
|
||||
### Full example with shared event loop group
|
||||
|
||||
```swift
|
||||
import CloudHypervisor
|
||||
import NIOPosix
|
||||
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
||||
defer { try? group.syncShutdownGracefully() }
|
||||
|
||||
let client = try CloudHypervisor.Client(
|
||||
socketPath: URL(filePath: "/run/ch/vm0.sock"),
|
||||
eventLoopGroup: group
|
||||
)
|
||||
|
||||
let info = try await client.vmInfo()
|
||||
print(info.state)
|
||||
```
|
||||
|
||||
## Supported Endpoints (v1)
|
||||
|
||||
### VMM
|
||||
|
||||
- `vmmPing() -> VmmPingResponse` — verify the VMM process is alive
|
||||
- `vmmShutdown()` — shut down the VMM process
|
||||
- `vmmInfo() -> VmmInfo` — query VMM-level metadata
|
||||
|
||||
### VM Lifecycle
|
||||
|
||||
- `vmCreate(_ config: VmConfig)` — define a new VM
|
||||
- `vmBoot()` — start the VM
|
||||
- `vmShutdown()` — gracefully shut down the VM
|
||||
- `vmInfo() -> VmInfo` — query VM state and configuration
|
||||
- `vmPause()` — pause a running VM
|
||||
- `vmResume()` — resume a paused VM
|
||||
|
||||
### Hotplug
|
||||
|
||||
- `vmAddDisk(_ config: DiskConfig) -> PciDeviceInfo` — hot-add a block device
|
||||
- `vmAddFs(_ config: FsConfig) -> PciDeviceInfo` — hot-add a virtio-fs share
|
||||
- `vmAddNet(_ config: NetConfig) -> PciDeviceInfo` — hot-add a network device
|
||||
- `vmAddVsock(_ config: VsockConfig) -> PciDeviceInfo` — hot-add a vsock device
|
||||
- `vmRemoveDevice(id: String)` — hot-remove a device by ID
|
||||
|
||||
## Minimum Supported cloud-hypervisor Version
|
||||
|
||||
The package targets the `/api/v1/` REST namespace. It is tested against **cloud-hypervisor v40** and later. Earlier releases may be missing endpoints or use incompatible JSON schemas.
|
||||
|
||||
## Error Model
|
||||
|
||||
All failures are reported through `CloudHypervisor.Error`:
|
||||
|
||||
- `.transport(any Swift.Error)` — a network or NIO-level failure before the HTTP response was received
|
||||
- `.http(status:body:)` — the server responded with a non-2xx HTTP status; `body` contains the raw response bytes
|
||||
- `.decoding(any Swift.Error, body:)` — the response had a 2xx status but JSON decoding failed; `body` is the raw bytes for diagnostics
|
||||
- `.invalidSocketPath(String)` — the URL passed to `Client.init` is not a `file://` URL
|
||||
|
||||
Non-2xx responses always produce `.http`, never a decode error, so callers can distinguish protocol-level errors from unexpected payloads.
|
||||
|
||||
## Concurrency
|
||||
|
||||
`Client` is `Sendable` and all endpoint methods are `async throws`. Each call opens a fresh TCP-over-UDS connection to cloud-hypervisor and closes it when the response is complete.
|
||||
|
||||
By default the client creates and owns a `MultiThreadedEventLoopGroup` and shuts it down in `deinit`. If you already have an event loop group (e.g. from NIO or another library), pass it via the `eventLoopGroup:` parameter — in that case the client does **not** shut the group down on `deinit`, leaving lifecycle management to the caller.
|
||||
|
||||
## Non-Goals (v1)
|
||||
|
||||
- Not a high-level VM orchestration layer — for that, use the `Containerization` library.
|
||||
- Not exhaustive coverage of cloud-hypervisor's full OpenAPI surface — only the 14 endpoints listed above are implemented; additional endpoints can be added incrementally.
|
||||
- No connection pooling — a fresh connection is opened per request, which is appropriate for low-volume control-plane use.
|
||||
- No streaming response bodies — response payloads are buffered in memory before decoding.
|
||||
@@ -0,0 +1,242 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension CloudHypervisor {
|
||||
// MARK: - ImageType
|
||||
|
||||
/// On-disk format of a `DiskConfig`'s backing file. When omitted on the
|
||||
/// wire, cloud-hypervisor defaults to `Unknown` and rejects writes to
|
||||
/// the disk (logging "Attempting to write to sector 0 on a disk without
|
||||
/// specifying image_type"); always set this explicitly.
|
||||
///
|
||||
/// Raw values match the Rust `block::ImageType` enum variants used in
|
||||
/// CH's JSON serialization (PascalCase) — these differ from the
|
||||
/// lowercase tokens accepted on the `--disk` CLI flag.
|
||||
public enum ImageType: String, Sendable, Codable, Equatable {
|
||||
case raw = "Raw"
|
||||
case qcow2 = "Qcow2"
|
||||
case fixedVhd = "FixedVhd"
|
||||
case vhdx = "Vhdx"
|
||||
case unknown = "Unknown"
|
||||
}
|
||||
|
||||
// MARK: - DiskConfig
|
||||
|
||||
/// Virtio-blk disk configuration.
|
||||
///
|
||||
/// Maps to `DiskConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct DiskConfig: Sendable, Codable, Equatable {
|
||||
/// Path to the disk image file.
|
||||
public var path: String
|
||||
/// Open the disk in read-only mode.
|
||||
public var readonly: Bool?
|
||||
/// Use O_DIRECT for disk I/O.
|
||||
public var direct: Bool?
|
||||
/// Enable IOMMU for this device.
|
||||
public var iommu: Bool?
|
||||
/// Optional device identifier.
|
||||
public var id: String?
|
||||
/// PCI segment to attach the device to.
|
||||
public var pciSegment: UInt16?
|
||||
/// On-disk format of the backing file.
|
||||
public var imageType: ImageType?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
readonly: Bool? = nil,
|
||||
direct: Bool? = nil,
|
||||
iommu: Bool? = nil,
|
||||
id: String? = nil,
|
||||
pciSegment: UInt16? = nil,
|
||||
imageType: ImageType? = nil
|
||||
) {
|
||||
self.path = path
|
||||
self.readonly = readonly
|
||||
self.direct = direct
|
||||
self.iommu = iommu
|
||||
self.id = id
|
||||
self.pciSegment = pciSegment
|
||||
self.imageType = imageType
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case readonly
|
||||
case direct
|
||||
case iommu
|
||||
case id
|
||||
case pciSegment = "pci_segment"
|
||||
case imageType = "image_type"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NetConfig
|
||||
|
||||
/// Virtio-net network device configuration.
|
||||
///
|
||||
/// Maps to `NetConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct NetConfig: Sendable, Codable, Equatable {
|
||||
/// TAP device name on the host.
|
||||
public var tap: String?
|
||||
/// IPv4 address for the device.
|
||||
public var ip: String?
|
||||
/// IPv4 subnet mask.
|
||||
public var mask: String?
|
||||
/// MAC address for the device.
|
||||
public var mac: String?
|
||||
/// Maximum transmission unit.
|
||||
public var mtu: Int?
|
||||
/// Number of virtio queues.
|
||||
public var numQueues: Int?
|
||||
/// Size of each virtio queue.
|
||||
public var queueSize: Int?
|
||||
/// Optional device identifier.
|
||||
public var id: String?
|
||||
|
||||
public init(
|
||||
tap: String? = nil,
|
||||
ip: String? = nil,
|
||||
mask: String? = nil,
|
||||
mac: String? = nil,
|
||||
mtu: Int? = nil,
|
||||
numQueues: Int? = nil,
|
||||
queueSize: Int? = nil,
|
||||
id: String? = nil
|
||||
) {
|
||||
self.tap = tap
|
||||
self.ip = ip
|
||||
self.mask = mask
|
||||
self.mac = mac
|
||||
self.mtu = mtu
|
||||
self.numQueues = numQueues
|
||||
self.queueSize = queueSize
|
||||
self.id = id
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tap
|
||||
case ip
|
||||
case mask
|
||||
case mac
|
||||
case mtu
|
||||
case numQueues = "num_queues"
|
||||
case queueSize = "queue_size"
|
||||
case id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FsConfig
|
||||
|
||||
/// Virtio-fs filesystem device configuration.
|
||||
///
|
||||
/// Maps to `FsConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct FsConfig: Sendable, Codable, Equatable {
|
||||
/// Filesystem tag used by the guest to mount.
|
||||
public var tag: String
|
||||
/// Path to the virtiofsd Unix socket.
|
||||
public var socket: String
|
||||
/// Number of virtio queues.
|
||||
public var numQueues: Int?
|
||||
/// Size of each virtio queue.
|
||||
public var queueSize: Int?
|
||||
/// Optional device identifier.
|
||||
public var id: String?
|
||||
/// PCI segment to attach the device to.
|
||||
public var pciSegment: UInt16?
|
||||
|
||||
public init(
|
||||
tag: String,
|
||||
socket: String,
|
||||
numQueues: Int? = nil,
|
||||
queueSize: Int? = nil,
|
||||
id: String? = nil,
|
||||
pciSegment: UInt16? = nil
|
||||
) {
|
||||
self.tag = tag
|
||||
self.socket = socket
|
||||
self.numQueues = numQueues
|
||||
self.queueSize = queueSize
|
||||
self.id = id
|
||||
self.pciSegment = pciSegment
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tag
|
||||
case socket
|
||||
case numQueues = "num_queues"
|
||||
case queueSize = "queue_size"
|
||||
case id
|
||||
case pciSegment = "pci_segment"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VsockConfig
|
||||
|
||||
/// Virtio-vsock configuration.
|
||||
///
|
||||
/// Maps to `VsockConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct VsockConfig: Sendable, Codable, Equatable {
|
||||
/// Context ID (CID) for the vsock device.
|
||||
public var cid: UInt32
|
||||
/// Path to the vsock Unix socket on the host.
|
||||
public var socket: String
|
||||
/// Enable IOMMU for this device.
|
||||
public var iommu: Bool?
|
||||
/// Optional device identifier.
|
||||
public var id: String?
|
||||
|
||||
public init(
|
||||
cid: UInt32,
|
||||
socket: String,
|
||||
iommu: Bool? = nil,
|
||||
id: String? = nil
|
||||
) {
|
||||
self.cid = cid
|
||||
self.socket = socket
|
||||
self.iommu = iommu
|
||||
self.id = id
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case cid
|
||||
case socket
|
||||
case iommu
|
||||
case id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PciDeviceInfo
|
||||
|
||||
/// PCI device identifier returned by Cloud Hypervisor after device add.
|
||||
///
|
||||
/// Maps to `PciDeviceInfo` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct PciDeviceInfo: Sendable, Codable, Equatable {
|
||||
/// Device identifier string.
|
||||
public var id: String
|
||||
/// PCI Bus:Device.Function address (e.g. `"0000:00:03.0"`).
|
||||
public var bdf: String
|
||||
|
||||
public init(id: String, bdf: String) {
|
||||
self.id = id
|
||||
self.bdf = bdf
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case bdf
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension CloudHypervisor {
|
||||
// MARK: - VmConfig
|
||||
|
||||
/// Top-level VM boot / create payload.
|
||||
///
|
||||
/// Maps to `VmConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct VmConfig: Sendable, Codable, Equatable {
|
||||
public var cpus: CpusConfig
|
||||
public var memory: MemoryConfig
|
||||
public var payload: PayloadConfig
|
||||
public var disks: [DiskConfig]?
|
||||
public var net: [NetConfig]?
|
||||
public var fs: [FsConfig]?
|
||||
public var vsock: VsockConfig?
|
||||
public var console: ConsoleConfig
|
||||
public var serial: ConsoleConfig
|
||||
|
||||
public init(
|
||||
cpus: CpusConfig,
|
||||
memory: MemoryConfig,
|
||||
payload: PayloadConfig,
|
||||
disks: [DiskConfig]? = nil,
|
||||
net: [NetConfig]? = nil,
|
||||
fs: [FsConfig]? = nil,
|
||||
vsock: VsockConfig? = nil,
|
||||
console: ConsoleConfig,
|
||||
serial: ConsoleConfig
|
||||
) {
|
||||
self.cpus = cpus
|
||||
self.memory = memory
|
||||
self.payload = payload
|
||||
self.disks = disks
|
||||
self.net = net
|
||||
self.fs = fs
|
||||
self.vsock = vsock
|
||||
self.console = console
|
||||
self.serial = serial
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case cpus
|
||||
case memory
|
||||
case payload
|
||||
case disks
|
||||
case net
|
||||
case fs
|
||||
case vsock
|
||||
case console
|
||||
case serial
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CpusConfig
|
||||
|
||||
/// CPU configuration for a VM.
|
||||
///
|
||||
/// Maps to `CpusConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct CpusConfig: Sendable, Codable, Equatable {
|
||||
/// Number of vCPUs to boot with.
|
||||
public var bootVcpus: Int
|
||||
/// Maximum number of vCPUs (for hotplug).
|
||||
public var maxVcpus: Int
|
||||
|
||||
public init(bootVcpus: Int, maxVcpus: Int) {
|
||||
self.bootVcpus = bootVcpus
|
||||
self.maxVcpus = maxVcpus
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case bootVcpus = "boot_vcpus"
|
||||
case maxVcpus = "max_vcpus"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MemoryConfig
|
||||
|
||||
/// Memory configuration for a VM.
|
||||
///
|
||||
/// Maps to `MemoryConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct MemoryConfig: Sendable, Codable, Equatable {
|
||||
/// RAM size in bytes.
|
||||
public var size: UInt64
|
||||
/// Hotplug memory size in bytes.
|
||||
public var hotplugSize: UInt64?
|
||||
/// Enable memory merging (KSM).
|
||||
public var mergeable: Bool?
|
||||
/// Use a shared memory mapping (`MAP_SHARED`). Required when any
|
||||
/// vhost-user device (e.g. virtio-fs / virtiofsd) is attached —
|
||||
/// CH otherwise rejects `vm.boot` with "Using vhost-user requires
|
||||
/// using shared memory or huge pages".
|
||||
public var shared: Bool?
|
||||
|
||||
public init(size: UInt64, hotplugSize: UInt64? = nil, mergeable: Bool? = nil, shared: Bool? = nil) {
|
||||
self.size = size
|
||||
self.hotplugSize = hotplugSize
|
||||
self.mergeable = mergeable
|
||||
self.shared = shared
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case size
|
||||
case hotplugSize = "hotplug_size"
|
||||
case mergeable
|
||||
case shared
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PayloadConfig
|
||||
|
||||
/// Kernel / initramfs / cmdline payload for a VM.
|
||||
///
|
||||
/// Maps to `PayloadConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct PayloadConfig: Sendable, Codable, Equatable {
|
||||
/// Path to the uncompressed kernel image (vmlinux).
|
||||
public var kernel: String
|
||||
/// Optional initramfs path.
|
||||
public var initramfs: String?
|
||||
/// Optional kernel command line.
|
||||
public var cmdline: String?
|
||||
|
||||
public init(kernel: String, initramfs: String? = nil, cmdline: String? = nil) {
|
||||
self.kernel = kernel
|
||||
self.initramfs = initramfs
|
||||
self.cmdline = cmdline
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case kernel
|
||||
case initramfs
|
||||
case cmdline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ConsoleConfig
|
||||
|
||||
/// Console / serial device configuration.
|
||||
///
|
||||
/// Maps to `ConsoleConfig` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct ConsoleConfig: Sendable, Codable, Equatable {
|
||||
/// Console I/O mode.
|
||||
///
|
||||
/// CH's OpenAPI spec uses these capitalized strings literally.
|
||||
public enum Mode: String, Codable, Sendable {
|
||||
case Off
|
||||
case Pty
|
||||
case Tty
|
||||
case File
|
||||
case Socket
|
||||
case Null
|
||||
}
|
||||
|
||||
public var mode: Mode
|
||||
/// Path to the output file when `mode == .File`.
|
||||
public var file: String?
|
||||
/// Path to the Unix socket when `mode == .Socket`.
|
||||
public var socket: String?
|
||||
|
||||
public init(mode: Mode, file: String? = nil, socket: String? = nil) {
|
||||
self.mode = mode
|
||||
self.file = file
|
||||
self.socket = socket
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case mode
|
||||
case file
|
||||
case socket
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension CloudHypervisor {
|
||||
// MARK: - VmState
|
||||
|
||||
/// Lifecycle state of a Cloud Hypervisor VM.
|
||||
///
|
||||
/// Maps to `VmState` in the Cloud Hypervisor OpenAPI spec.
|
||||
/// The raw values match CH's literal strings exactly (capitalized).
|
||||
public enum VmState: String, Sendable, Codable, Equatable {
|
||||
case Created
|
||||
case Running
|
||||
case Shutdown
|
||||
case Paused
|
||||
case BreakPoint
|
||||
}
|
||||
|
||||
// MARK: - VmInfo
|
||||
|
||||
/// Response body for `GET /vm.info`.
|
||||
///
|
||||
/// Maps to `VmInfo` in the Cloud Hypervisor OpenAPI spec.
|
||||
///
|
||||
/// Note: the `device_tree` map (`[String: VmInfoDeviceNode]`) from the
|
||||
/// upstream OpenAPI spec is omitted in this v1 implementation — no current
|
||||
/// endpoint consumers require it. Add when needed.
|
||||
public struct VmInfo: Sendable, Codable, Equatable {
|
||||
/// The boot configuration used for this VM.
|
||||
public var config: VmConfig
|
||||
/// Current lifecycle state.
|
||||
public var state: VmState
|
||||
/// Actual memory size in bytes as reported by the VMM, if available.
|
||||
public var memoryActualSize: UInt64?
|
||||
|
||||
public init(config: VmConfig, state: VmState, memoryActualSize: UInt64? = nil) {
|
||||
self.config = config
|
||||
self.state = state
|
||||
self.memoryActualSize = memoryActualSize
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case config
|
||||
case state
|
||||
case memoryActualSize = "memory_actual_size"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VmmPingResponse
|
||||
|
||||
/// Response body for `GET /vmm.ping`.
|
||||
///
|
||||
/// Maps to `VmmPingResponse` in the Cloud Hypervisor OpenAPI spec.
|
||||
public struct VmmPingResponse: Sendable, Codable, Equatable {
|
||||
/// Cloud Hypervisor version string (e.g. `"v40.0"`).
|
||||
public var version: String
|
||||
/// PID of the VMM process, if provided.
|
||||
public var pid: Int?
|
||||
/// List of compiled-in feature flags, if provided.
|
||||
public var features: [String]?
|
||||
/// Build-time version string, if provided.
|
||||
public var buildVersion: String?
|
||||
|
||||
public init(version: String, pid: Int? = nil, features: [String]? = nil, buildVersion: String? = nil) {
|
||||
self.version = version
|
||||
self.pid = pid
|
||||
self.features = features
|
||||
self.buildVersion = buildVersion
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case pid
|
||||
case features
|
||||
case buildVersion = "build_version"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VmmInfo
|
||||
|
||||
/// Response body for `GET /vmm.info`.
|
||||
///
|
||||
/// Maps to a subset of the `VmmInfo` schema in the Cloud Hypervisor OpenAPI
|
||||
/// spec. Only the fields needed by v1 consumers are included (YAGNI).
|
||||
public struct VmmInfo: Sendable, Codable, Equatable {
|
||||
/// Cloud Hypervisor version string (e.g. `"v40.0"`).
|
||||
public var version: String
|
||||
/// PID of the VMM process, if provided.
|
||||
public var pid: Int?
|
||||
/// Build-time version string, if provided.
|
||||
public var buildVersion: String?
|
||||
/// The currently-running VM's boot configuration, if a VM exists.
|
||||
public var config: VmConfig?
|
||||
|
||||
public init(version: String, pid: Int? = nil, buildVersion: String? = nil, config: VmConfig? = nil) {
|
||||
self.version = version
|
||||
self.pid = pid
|
||||
self.buildVersion = buildVersion
|
||||
self.config = config
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case pid
|
||||
case buildVersion = "build_version"
|
||||
case config
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user