mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-02 04:01:12 +02:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d45ef38cf7 | ||
|
|
e26b376d51 | ||
|
|
8f8a24ad19 | ||
|
|
29e0606ea3 | ||
|
|
594c6d74cd | ||
|
|
fc159c9992 | ||
|
|
863e3c2925 | ||
|
|
372affb0dc |
+1
-1
@@ -153,7 +153,7 @@ task:
|
||||
name: Deploy Documentation
|
||||
only_if: $CIRRUS_BRANCH == 'main'
|
||||
container:
|
||||
image: ghcr.io/cirruslabs/mkdocs-material-insiders:latest
|
||||
image: ghcr.io/squidfunk/mkdocs-material:latest
|
||||
registry_config: ENCRYPTED[!cf1a0f25325aa75bad3ce6ebc890bc53eb0044c02efa70d8cefb83ba9766275a994b4831706c52630a0692b2fa9cfb9e!]
|
||||
env:
|
||||
DEPLOY_TOKEN: ENCRYPTED[!45ed45666558902ed1c2400add734ec063103bec31841847e8c8764802fca229bfa6d85c690e16ad159e047574b48793!]
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "0da1cc30fa3c41e8c2e2edcdd55706549908275a54f681203e0eeade279deab9",
|
||||
"originHash" : "061dfe6cdf4e6dbf32b51c5e7023c4ae69726dcafb42a35b34e5489b0338c17f",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "antlr4",
|
||||
@@ -49,10 +49,10 @@
|
||||
{
|
||||
"identity" : "opentelemetry-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cirruslabs/opentelemetry-swift",
|
||||
"location" : "https://github.com/open-telemetry/opentelemetry-swift",
|
||||
"state" : {
|
||||
"branch" : "use-feedback-handler",
|
||||
"revision" : "2040f383e2a8b0a568a7617d147f8f1e23abb298"
|
||||
"branch" : "main",
|
||||
"revision" : "ed37be9525081509ab62410d38b705c2b3f0d5a4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/jozefizso/swift-xattr", from: "3.0.0"),
|
||||
.package(url: "https://github.com/grpc/grpc-swift.git", .upToNextMajor(from: "1.27.0")),
|
||||
.package(url: "https://buf.build/gen/swift/git/1.27.1-20260114140118-bd09c26a260f.1/cirruslabs_tart-guest-agent_grpc_swift.git", branch: "main"),
|
||||
.package(url: "https://github.com/cirruslabs/opentelemetry-swift", branch: "use-feedback-handler"),
|
||||
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", branch: "main"),
|
||||
.package(url: "https://github.com/open-telemetry/opentelemetry-swift-core", from: "2.3.0"),
|
||||
|
||||
],
|
||||
|
||||
@@ -47,8 +47,16 @@ struct Exec: AsyncParsableCommand {
|
||||
try! group.syncShutdownGracefully()
|
||||
}
|
||||
|
||||
// Change the current working directory to a VM's base directory
|
||||
// to work around Unix domain socket 104 byte limitation [1]
|
||||
//
|
||||
// [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/
|
||||
if let baseURL = vmDir.controlSocketURL.baseURL {
|
||||
FileManager.default.changeCurrentDirectoryPath(baseURL.path())
|
||||
}
|
||||
|
||||
let channel = try GRPCChannelPool.with(
|
||||
target: .unixDomainSocket(vmDir.controlSocketURL.path()),
|
||||
target: .unixDomainSocket(vmDir.controlSocketURL.relativePath),
|
||||
transportSecurity: .plaintext,
|
||||
eventLoopGroup: group,
|
||||
)
|
||||
|
||||
@@ -68,7 +68,15 @@ struct IP: AsyncParsableCommand {
|
||||
throw RuntimeError.Generic("Cannot perform IP resolution via Tart Guest Agent when control socket URL is not set")
|
||||
}
|
||||
|
||||
if let ip = try await AgentResolver.ResolveIP(controlSocketURL) {
|
||||
// Change the current working directory to a VM's base directory
|
||||
// to work around Unix domain socket 104 byte limitation [1]
|
||||
//
|
||||
// [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/
|
||||
if let baseURL = controlSocketURL.baseURL {
|
||||
FileManager.default.changeCurrentDirectoryPath(baseURL.path())
|
||||
}
|
||||
|
||||
if let ip = try await AgentResolver.ResolveIP(controlSocketURL.relativePath) {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ fileprivate struct VMInfo: Encodable {
|
||||
let Name: String
|
||||
let Disk: Int
|
||||
let Size: Int
|
||||
let SizeOnDisk: Int
|
||||
let Accessed: String
|
||||
let Running: Bool
|
||||
let State: String
|
||||
}
|
||||
@@ -39,13 +39,29 @@ struct List: AsyncParsableCommand {
|
||||
|
||||
if source == nil || source == "local" {
|
||||
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
||||
try VMInfo(Source: "local", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), SizeOnDisk: vmDir.allocatedSizeGB() - vmDir.deduplicatedSizeGB(), Running: vmDir.running(), State: vmDir.state().rawValue)
|
||||
try VMInfo(
|
||||
Source: "local",
|
||||
Name: name,
|
||||
Disk: vmDir.sizeGB(),
|
||||
Size: vmDir.allocatedSizeGB(),
|
||||
Accessed: formatAccessDate(try vmDir.accessDate()),
|
||||
Running: vmDir.running(),
|
||||
State: vmDir.state().rawValue
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if source == nil || source == "oci" {
|
||||
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
||||
try VMInfo(Source: "OCI", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), SizeOnDisk: vmDir.allocatedSizeGB() - vmDir.deduplicatedSizeGB(), Running: vmDir.running(), State: vmDir.state().rawValue)
|
||||
try VMInfo(
|
||||
Source: "OCI",
|
||||
Name: name,
|
||||
Disk: vmDir.sizeGB(),
|
||||
Size: vmDir.allocatedSizeGB(),
|
||||
Accessed: formatAccessDate(try vmDir.accessDate()),
|
||||
Running: vmDir.running(),
|
||||
State: vmDir.state().rawValue
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,4 +77,16 @@ struct List: AsyncParsableCommand {
|
||||
private func sortedInfos(_ infos: [VMInfo]) -> [VMInfo] {
|
||||
infos.sorted(by: { left, right in left.Name < right.Name })
|
||||
}
|
||||
|
||||
private func formatAccessDate(_ accessDate: Date) -> String {
|
||||
switch format {
|
||||
case .text:
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
return formatter.localizedString(for: accessDate, relativeTo: Date())
|
||||
case .json:
|
||||
let formatter = ISO8601DateFormatter()
|
||||
return formatter.string(from: accessDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,13 +243,6 @@ struct Run: AsyncParsableCommand {
|
||||
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
|
||||
var netHost: Bool = false
|
||||
|
||||
@Option(help: ArgumentHelp("Use externally managed connected datagram socket file descriptor for VM networking (e.g. --net-fd=3)", discussion: """
|
||||
This option allows integrating Tart with externally launched networking helpers.
|
||||
|
||||
The provided file descriptor must reference a connected datagram socket.
|
||||
""", valueName: "fd", visibility: .hidden))
|
||||
var netFd: Int32?
|
||||
|
||||
@Option(help: ArgumentHelp("Set the root disk options (e.g. --root-disk-opts=\"ro\" or --root-disk-opts=\"caching=cached,sync=none\")",
|
||||
discussion: """
|
||||
Options are comma-separated and are as follows:
|
||||
@@ -302,19 +295,14 @@ struct Run: AsyncParsableCommand {
|
||||
netSoftnet = true
|
||||
}
|
||||
|
||||
if let netFd = netFd, netFd < 0 {
|
||||
throw ValidationError("--net-fd must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
// Check that no more than one network option is specified
|
||||
var netFlags = 0
|
||||
if netBridged.count > 0 { netFlags += 1 }
|
||||
if netSoftnet { netFlags += 1 }
|
||||
if netHost { netFlags += 1 }
|
||||
if netFd != nil { netFlags += 1 }
|
||||
|
||||
if netFlags > 1 {
|
||||
throw ValidationError("--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
|
||||
}
|
||||
|
||||
if graphics && noGraphics {
|
||||
@@ -632,10 +620,6 @@ struct Run: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
|
||||
if let netFd = netFd {
|
||||
return try NetworkFD(fd: netFd)
|
||||
}
|
||||
|
||||
var softnetExtraArguments: [String] = []
|
||||
|
||||
if let netSoftnetAllow = netSoftnetAllow {
|
||||
|
||||
@@ -21,8 +21,16 @@ class ControlSocket {
|
||||
// if any, otherwise we may get the "address already in use" error
|
||||
try? FileManager.default.removeItem(atPath: controlSocketURL.path())
|
||||
|
||||
// Change the current working directory to a VM's base directory
|
||||
// to work around Unix domain socket 104 byte limitation [1]
|
||||
//
|
||||
// [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/
|
||||
if let baseURL = controlSocketURL.baseURL {
|
||||
FileManager.default.changeCurrentDirectoryPath(baseURL.path())
|
||||
}
|
||||
|
||||
let serverChannel = try await ServerBootstrap(group: eventLoopGroup)
|
||||
.bind(unixDomainSocketPath: controlSocketURL.path()) { childChannel in
|
||||
.bind(unixDomainSocketPath: controlSocketURL.relativePath) { childChannel in
|
||||
childChannel.eventLoop.makeCompletedFuture {
|
||||
return try NIOAsyncChannel<ByteBuffer, ByteBuffer>(
|
||||
wrappingChannelSynchronously: childChannel
|
||||
|
||||
@@ -15,7 +15,7 @@ class StdinCredentials {
|
||||
return (user, password)
|
||||
}
|
||||
|
||||
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 1024, isSensitive: Bool) throws -> String {
|
||||
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 8192, isSensitive: Bool) throws -> String {
|
||||
var buf = [CChar](repeating: 0, count: maxCharacters + 1 /* sentinel */ + 1 /* NUL */)
|
||||
guard let rawCredential = readpassphrase(prompt, &buf, buf.count, isSensitive ? RPP_ECHO_OFF : RPP_ECHO_ON) else {
|
||||
throw StdinCredentialsError.CredentialRequired(which: name)
|
||||
|
||||
@@ -6,15 +6,15 @@ import Cirruslabs_TartGuestAgent_Apple_Swift
|
||||
import Cirruslabs_TartGuestAgent_Grpc_Swift
|
||||
|
||||
class AgentResolver {
|
||||
static func ResolveIP(_ controlSocketURL: URL) async throws -> IPv4Address? {
|
||||
static func ResolveIP(_ controlSocketPath: String) async throws -> IPv4Address? {
|
||||
do {
|
||||
return try await resolveIP(controlSocketURL)
|
||||
return try await resolveIP(controlSocketPath)
|
||||
} catch let error as GRPCConnectionPoolError {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func resolveIP(_ controlSocketURL: URL) async throws -> IPv4Address? {
|
||||
private static func resolveIP(_ controlSocketPath: String) async throws -> IPv4Address? {
|
||||
// Create a gRPC channel connected to the VM's control socket
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
defer {
|
||||
@@ -22,7 +22,7 @@ class AgentResolver {
|
||||
}
|
||||
|
||||
let channel = try GRPCChannelPool.with(
|
||||
target: .unixDomainSocket(controlSocketURL.path()),
|
||||
target: .unixDomainSocket(controlSocketPath),
|
||||
transportSecurity: .plaintext,
|
||||
eventLoopGroup: group,
|
||||
)
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import Semaphore
|
||||
import Virtualization
|
||||
|
||||
class NetworkFD: Network {
|
||||
private let fd: Int32
|
||||
|
||||
init(fd: Int32) throws {
|
||||
self.fd = fd
|
||||
|
||||
try Self.validateFD(fd)
|
||||
try Self.validateSocketType(fd)
|
||||
try Self.validateConnected(fd)
|
||||
}
|
||||
|
||||
func attachments() -> [VZNetworkDeviceAttachment] {
|
||||
[VZFileHandleNetworkDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd))]
|
||||
}
|
||||
|
||||
func run(_ sema: AsyncSemaphore) throws {
|
||||
// no-op, only used for Softnet
|
||||
}
|
||||
|
||||
func stop() async throws {
|
||||
// no-op, only used for Softnet
|
||||
}
|
||||
|
||||
private static func validateFD(_ fd: Int32) throws {
|
||||
if fcntl(fd, F_GETFD) == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): file descriptor is not open (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateSocketType(_ fd: Int32) throws {
|
||||
var socketType: Int32 = 0
|
||||
var optionLength = socklen_t(MemoryLayout<Int32>.size)
|
||||
|
||||
if getsockopt(fd, SOL_SOCKET, SO_TYPE, &socketType, &optionLength) == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): file descriptor must reference a socket (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
|
||||
if socketType != SOCK_DGRAM {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): expected SOCK_DGRAM socket, got \(socketType)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateConnected(_ fd: Int32) throws {
|
||||
var address = sockaddr_storage()
|
||||
var addressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
|
||||
getpeername(fd, sockaddrPointer, &addressLength)
|
||||
}
|
||||
}
|
||||
|
||||
if result == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): socket must be connected (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func errnoDescription(_ code: CInt) -> String {
|
||||
String(cString: strerror(code))
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ struct VMDirectory: Prunable {
|
||||
baseURL.appendingPathComponent("manifest.json")
|
||||
}
|
||||
var controlSocketURL: URL {
|
||||
baseURL.appendingPathComponent("control.sock")
|
||||
URL(fileURLWithPath: "control.sock", relativeTo: baseURL)
|
||||
}
|
||||
|
||||
var explicitlyPulledMark: URL {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
final class NetworkFDTests: XCTestCase {
|
||||
func testAcceptsConnectedDatagramSocket() throws {
|
||||
let (fdLeft, fdRight) = try makeDatagramSocketPair()
|
||||
defer {
|
||||
_ = close(fdLeft)
|
||||
_ = close(fdRight)
|
||||
}
|
||||
|
||||
let network = try NetworkFD(fd: fdLeft)
|
||||
|
||||
XCTAssertEqual(network.attachments().count, 1)
|
||||
}
|
||||
|
||||
func testRejectsClosedFileDescriptor() throws {
|
||||
let (fdLeft, fdRight) = try makeDatagramSocketPair()
|
||||
defer { _ = close(fdRight) }
|
||||
|
||||
_ = close(fdLeft)
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fdLeft)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "file descriptor is not open")
|
||||
}
|
||||
}
|
||||
|
||||
func testRejectsNonSocketFileDescriptor() throws {
|
||||
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
XCTAssertTrue(FileManager.default.createFile(atPath: fileURL.path, contents: Data()))
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
let fd = open(fileURL.path, O_RDONLY)
|
||||
XCTAssertGreaterThanOrEqual(fd, 0)
|
||||
defer { _ = close(fd) }
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fd)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "must reference a socket")
|
||||
}
|
||||
}
|
||||
|
||||
func testRejectsUnconnectedDatagramSocket() throws {
|
||||
let fd = socket(AF_UNIX, SOCK_DGRAM, 0)
|
||||
XCTAssertGreaterThanOrEqual(fd, 0)
|
||||
defer { _ = close(fd) }
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fd)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "socket must be connected")
|
||||
}
|
||||
}
|
||||
|
||||
private func makeDatagramSocketPair() throws -> (Int32, Int32) {
|
||||
var fds: [Int32] = [-1, -1]
|
||||
let result = socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds)
|
||||
|
||||
if result == -1 {
|
||||
throw RuntimeError.VMConfigurationError("failed to create a datagram socketpair for tests")
|
||||
}
|
||||
|
||||
return (fds[0], fds[1])
|
||||
}
|
||||
|
||||
private func assertVMConfigurationError(
|
||||
_ error: Error,
|
||||
contains expectedSubstring: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
guard case RuntimeError.VMConfigurationError(let message) = error else {
|
||||
XCTFail("Expected RuntimeError.VMConfigurationError, got \(error)", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
message.contains(expectedSubstring),
|
||||
"Expected message to contain \"\(expectedSubstring)\", got \"\(message)\"",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
final class RunNetworkValidationTests: XCTestCase {
|
||||
func testNetFdRejectsNegativeValue() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd=-1"])) { error in
|
||||
self.assertError(error, contains: "--net-fd must be greater than or equal to 0")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetBridged() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-bridged=en0"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetSoftnet() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-softnet"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetHost() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-host"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
private func assertError(
|
||||
_ error: Error,
|
||||
contains expectedSubstring: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
XCTAssertTrue(
|
||||
String(describing: error).contains(expectedSubstring),
|
||||
"Expected error to contain \"\(expectedSubstring)\", got \"\(error)\"",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -60,10 +60,14 @@ device without a physical display connected. For example, a Mac Mini with a HDMI
|
||||
but a Mac Mini on a desk with a connected physical display is considered a personal computer. **Usage on personal computers
|
||||
and before reaching the 100 CPU cores limit is royalty-free and does not have the viral properties of AGPL.**
|
||||
|
||||
!!! note "Pricing update"
|
||||
This post announced Tart licensing in February 2023 and originally listed monthly prices.
|
||||
Pricing has since changed to yearly billing. See [Licensing and Support](../../licensing.md#license-tiers) for the latest terms.
|
||||
|
||||
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold Tier License](../../licensing.md#license-tiers),
|
||||
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](../../licensing.md#license-tiers)
|
||||
(\$3000 per month) will be required, and for organizations that exceed 3000 CPU cores, a custom [Diamond Tier License](../../licensing.md#license-tiers)
|
||||
(\$1 per core per month) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.**
|
||||
which costs \$12,000 per year. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](../../licensing.md#license-tiers)
|
||||
(\$36,000 per year) will be required, and for organizations that exceed 3000 CPU cores, a custom [Diamond Tier License](../../licensing.md#license-tiers)
|
||||
(\$12 per core per year) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.**
|
||||
|
||||
## Have we considered alternatives?
|
||||
|
||||
|
||||
+1
-15
@@ -77,20 +77,6 @@ Note: that accessing host is only possible with the default NAT network. If you
|
||||
[Softnet](https://github.com/cirruslabs/softnet) (via `tart run --net-softnet <VM NAME>)`, then the network isolation
|
||||
is stricter and it's not possible to access the host.
|
||||
|
||||
## Using externally managed networking (`--net-fd`)
|
||||
|
||||
For advanced integrations, `tart run` can consume a pre-opened connected datagram socket via `--net-fd`.
|
||||
|
||||
Unlike `--net-softnet`, Tart will not launch Softnet or configure Softnet permissions in this mode.
|
||||
|
||||
External launcher is responsible for:
|
||||
|
||||
* creating a connected datagram socketpair (for example, `socketpair(AF_UNIX, SOCK_DGRAM, ...)`)
|
||||
* starting `softnet --vm-fd ...` (or another networking helper) with one end of that socketpair
|
||||
* starting `tart run --net-fd <FD> <VM NAME>` with the other end inherited into Tart
|
||||
|
||||
If the file descriptor is invalid, not a datagram socket, or not connected, `tart run` fails fast.
|
||||
|
||||
## Changing the default NAT subnet
|
||||
|
||||
To change the default network to `192.168.77.1`:
|
||||
@@ -198,7 +184,7 @@ security unlock-keychain login.keychain
|
||||
|
||||
This command also supports the `-p` command-line argument that allows you to supply a password and unlock non-interactively, which is great for scripts.
|
||||
|
||||
Alternatively, you can pass the credentials via the environment variables, see [Registry Authorization](integrations/vm-management.md#registry-authorization) for more details on how to do that.
|
||||
Alternatively, you can pass the credentials via the environment variables, see [Registry Authorization](quick-start.md#registry-authorization) for more details on how to do that.
|
||||
|
||||
## How is Tart different from Anka?
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Automating VM image building with Packer
|
||||
description: Use Packer to build custom VM images, configure VMs and work with remote OCI registries.
|
||||
---
|
||||
|
||||
Please refer to [Tart Packer Plugin repository](https://github.com/cirruslabs/packer-plugin-tart) for setup instructions.
|
||||
Here is an example of a template to build a local image based of a remote image:
|
||||
|
||||
```hcl
|
||||
packer {
|
||||
required_plugins {
|
||||
tart = {
|
||||
version = ">= 0.5.3"
|
||||
source = "github.com/cirruslabs/tart"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source "tart-cli" "tart" {
|
||||
vm_base_name = "ghcr.io/cirruslabs/macos-sequoia-base:latest"
|
||||
vm_name = "my-custom-sequoia"
|
||||
cpu_count = 4
|
||||
memory_gb = 8
|
||||
disk_size_gb = 70
|
||||
ssh_password = "admin"
|
||||
ssh_timeout = "120s"
|
||||
ssh_username = "admin"
|
||||
}
|
||||
|
||||
build {
|
||||
sources = ["source.tart-cli.tart"]
|
||||
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'Disabling spotlight indexing...'", "sudo mdutil -a -i off"]
|
||||
}
|
||||
|
||||
# more provisioners
|
||||
}
|
||||
```
|
||||
|
||||
Here is a [repository with Packer templates](https://github.com/cirruslabs/macos-image-templates) used to build [all the images managed by us](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos).
|
||||
@@ -1,143 +0,0 @@
|
||||
---
|
||||
title: Managing Virtual Machine
|
||||
description: Use Packer to build custom VM images, configure VMs and work with remote OCI registries.
|
||||
---
|
||||
|
||||
# Managing Virtual Machine
|
||||
|
||||
## Creating from scratch
|
||||
|
||||
Tart supports macOS and Linux virtual machines. All commands like `run` and `pull` work the same way regardless of the underlying OS a particular VM image has.
|
||||
The only difference is how such VM images are created. Please check sections below for [macOS](#creating-a-macos-vm-image-from-scratch) and [Linux](#creating-a-linux-vm-image-from-scratch) instructions.
|
||||
|
||||
### Creating a macOS VM image from scratch
|
||||
|
||||
Tart can create VMs from `*.ipsw` files. You can download a specific `*.ipsw` file [here](https://ipsw.me/) or you can
|
||||
use `latest` instead of a path to `*.ipsw` to download the latest available version:
|
||||
|
||||
```bash
|
||||
tart create --from-ipsw=latest sequoia-vanilla
|
||||
tart run sequoia-vanilla
|
||||
```
|
||||
|
||||
After the initial booting of the VM, you'll need to manually go through the macOS installation process. As a convention we recommend creating an `admin` user with an `admin` password. After the regular installation please do some additional modifications in the VM:
|
||||
|
||||
1. Enable Auto-Login. Users & Groups -> Login Options -> Automatic login -> admin.
|
||||
2. Allow SSH. Sharing -> Remote Login
|
||||
3. Disable Lock Screen. Preferences -> Lock Screen -> disable "Require Password" after 5.
|
||||
4. Disable Screen Saver.
|
||||
5. Run `sudo visudo` in Terminal, find `%admin ALL=(ALL) ALL` add `admin ALL=(ALL) NOPASSWD: ALL` to allow sudo without a password.
|
||||
|
||||
### Creating a Linux VM image from scratch
|
||||
|
||||
Linux VMs are supported on hosts running macOS 13.0 (Ventura) or newer.
|
||||
|
||||
```bash
|
||||
# Create a bare VM
|
||||
tart create --linux ubuntu
|
||||
|
||||
# Install Ubuntu
|
||||
tart run --disk focal-desktop-arm64.iso ubuntu
|
||||
|
||||
# Run VM
|
||||
tart run ubuntu
|
||||
```
|
||||
|
||||
After the initial setup please make sure your VM can be SSH-ed into by running the following commands inside your VM:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y openssh-server
|
||||
sudo ufw allow ssh
|
||||
```
|
||||
|
||||
## Configuring a VM
|
||||
|
||||
By default, a Tart VM uses 2 CPUs and 4 GB of memory with a `1024x768` display. This can be changed after VM creation with `tart set` command.
|
||||
Please refer to `tart set --help` for additional details.
|
||||
|
||||
## Building with Packer
|
||||
|
||||
Please refer to [Tart Packer Plugin repository](https://github.com/cirruslabs/packer-plugin-tart) for setup instructions.
|
||||
Here is an example of a template to build a local image based of a remote image:
|
||||
|
||||
```hcl
|
||||
packer {
|
||||
required_plugins {
|
||||
tart = {
|
||||
version = ">= 0.5.3"
|
||||
source = "github.com/cirruslabs/tart"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source "tart-cli" "tart" {
|
||||
vm_base_name = "ghcr.io/cirruslabs/macos-sequoia-base:latest"
|
||||
vm_name = "my-custom-sequoia"
|
||||
cpu_count = 4
|
||||
memory_gb = 8
|
||||
disk_size_gb = 70
|
||||
ssh_password = "admin"
|
||||
ssh_timeout = "120s"
|
||||
ssh_username = "admin"
|
||||
}
|
||||
|
||||
build {
|
||||
sources = ["source.tart-cli.tart"]
|
||||
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'Disabling spotlight indexing...'", "sudo mdutil -a -i off"]
|
||||
}
|
||||
|
||||
# more provisioners
|
||||
}
|
||||
```
|
||||
|
||||
Here is a [repository with Packer templates](https://github.com/cirruslabs/macos-image-templates) used to build [all the images managed by us](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos).
|
||||
|
||||
## Working with a Remote OCI Container Registry
|
||||
|
||||
Tart supports interacting with Open Container Initiative (OCI) registries, but only runs images created and pushed by Tart. This means images created for container engines, like Docker, can't be pulled. Instead, create a custom image as documented above.
|
||||
|
||||
For example, let's say you want to push/pull images to an OCI registry hosted at `https://acme.io/`.
|
||||
|
||||
### Registry Authorization
|
||||
|
||||
First, you need to login to `acme.io` with the `tart login` command:
|
||||
|
||||
```bash
|
||||
tart login acme.io
|
||||
```
|
||||
|
||||
If you login to your registry with OAuth, you may need to create an access token to use as the password.
|
||||
Credentials are securely stored in Keychain.
|
||||
|
||||
In addition, Tart supports [Docker credential helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers)
|
||||
if defined in `~/.docker/config.json`.
|
||||
|
||||
Finally, `TART_REGISTRY_USERNAME` and `TART_REGISTRY_PASSWORD` environment variables allow to override authorization
|
||||
for all registries which might useful for integrating with your CI's secret management.
|
||||
|
||||
### Pushing a Local Image
|
||||
|
||||
Once credentials are saved for `acme.io`, run the following command to push a local images remotely with two tags:
|
||||
|
||||
```bash
|
||||
tart push my-local-vm-name acme.io/remoteorg/name:latest acme.io/remoteorg/name:v1.0.0
|
||||
```
|
||||
|
||||
### Pulling a Remote Image
|
||||
|
||||
You can either pull an image:
|
||||
|
||||
```bash
|
||||
tart pull acme.io/remoteorg/name:latest
|
||||
```
|
||||
|
||||
or create a VM from a remote image:
|
||||
|
||||
```bash
|
||||
tart clone acme.io/remoteorg/name:latest my-local-vm-name
|
||||
```
|
||||
|
||||
If the specified image is not already present, this invocation calls the `tart pull` implicitly before cloning.
|
||||
+1
-1
@@ -14,7 +14,7 @@ This page covers Terms of Service only for Cirrus Runners and Tart Documentation
|
||||
Cirrus Labs Inc ("Cirrus Labs") operates the [Cirrus Runners service](https://cirrus-runners.app/) which we hope you use.
|
||||
If you use it, please use it responsibly. If you don't, we'll have to terminate your subscription.
|
||||
|
||||
For paid plans, you'll be charged on a monthly basis. You can cancel anytime, but there are no refunds.
|
||||
For paid plans, you'll be charged on a yearly basis. You can cancel anytime, but there are no refunds.
|
||||
|
||||
The Terms of Service and our prices can change at any time unless specified in your agreement. We'll warn you 30 days in advance of any price changes.
|
||||
We'll try to warn you about major changes to the Terms of Service, but we make no guarantees.
|
||||
|
||||
+3
-3
@@ -36,19 +36,19 @@ Free Tier license has a 100 CPU core limit for Tart and 4 Orchard Workers limit
|
||||
|
||||
### Gold Tier
|
||||
|
||||
If an organization wishes to exceed the limits of the Free Tier license, a purchase of the [Gold Tier License](#get-the-license) is required, which costs \$1000 per month.
|
||||
If an organization wishes to exceed the limits of the Free Tier license, a purchase of the [Gold Tier License](#get-the-license) is required, which costs \$12,000 per year.
|
||||
|
||||
Gold Tier license has a 500 CPU core limit for Tart and 20 Orchard Workers limit for Orchard.
|
||||
|
||||
### Platinum Tier
|
||||
|
||||
If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$3000 per month.
|
||||
If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$36,000 per year.
|
||||
|
||||
Platinum Tier license has a 3,000 CPU core limit for Tart and 200 Orchard Workers limit for Orchard.
|
||||
|
||||
### Diamond Tier
|
||||
|
||||
For organizations that wish to exceed the limits of the Platinum Tier license, a purchase of a [custom Diamond Tier License](#get-the-license) is required, which costs \$1 per CPU core per month and gives the ability to run unlimited Orchard Workers.
|
||||
For organizations that wish to exceed the limits of the Platinum Tier license, a purchase of a [custom Diamond Tier License](#get-the-license) is required, which costs \$12 per CPU core per year and gives the ability to run unlimited Orchard Workers.
|
||||
|
||||
## Get the license
|
||||
|
||||
|
||||
@@ -99,6 +99,57 @@ ssh admin@$(tart ip sequoia-base)
|
||||
sshpass -p admin ssh -o "StrictHostKeyChecking no" -o "UserKnownHostsFile=/dev/null" admin@$(tart ip sequoia-base) < script.sh
|
||||
```
|
||||
|
||||
## Creating VM images
|
||||
|
||||
Tart supports macOS and Linux virtual machines. All commands like `run` and `pull` work the same way regardless of the underlying OS a particular VM image has.
|
||||
The only difference is how such VM images are created. Please check sections below for [macOS](#creating-a-macos-vm-image-from-scratch) and [Linux](#creating-a-linux-vm-image-from-scratch) instructions.
|
||||
|
||||
### Creating a macOS VM image from scratch
|
||||
|
||||
Tart can create VMs from `*.ipsw` files. You can download a specific `*.ipsw` file [here](https://ipsw.me/) or you can
|
||||
use `latest` instead of a path to `*.ipsw` to download the latest available version:
|
||||
|
||||
```bash
|
||||
tart create --from-ipsw=latest sequoia-vanilla
|
||||
tart run sequoia-vanilla
|
||||
```
|
||||
|
||||
After the initial booting of the VM, you'll need to manually go through the macOS installation process. As a convention we recommend creating an `admin` user with an `admin` password. After the regular installation please do some additional modifications in the VM:
|
||||
|
||||
1. Enable Auto-Login. Users & Groups -> Login Options -> Automatic login -> admin.
|
||||
2. Allow SSH. Sharing -> Remote Login
|
||||
3. Disable Lock Screen. Preferences -> Lock Screen -> disable "Require Password" after 5.
|
||||
4. Disable Screen Saver.
|
||||
5. Run `sudo visudo` in Terminal, find `%admin ALL=(ALL) ALL` add `admin ALL=(ALL) NOPASSWD: ALL` to allow sudo without a password.
|
||||
|
||||
### Creating a Linux VM image from scratch
|
||||
|
||||
Linux VMs are supported on hosts running macOS 13.0 (Ventura) or newer.
|
||||
|
||||
```bash
|
||||
# Create a bare VM
|
||||
tart create --linux ubuntu
|
||||
|
||||
# Install Ubuntu
|
||||
tart run --disk focal-desktop-arm64.iso ubuntu
|
||||
|
||||
# Run VM
|
||||
tart run ubuntu
|
||||
```
|
||||
|
||||
After the initial setup please make sure your VM can be SSH-ed into by running the following commands inside your VM:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y openssh-server
|
||||
sudo ufw allow ssh
|
||||
```
|
||||
|
||||
### Configuring a VM
|
||||
|
||||
By default, a Tart VM uses 2 CPUs and 4 GB of memory with a `1024x768` display. This can be changed after VM creation with `tart set` command.
|
||||
Please refer to `tart set --help` for additional details.
|
||||
|
||||
## Mounting directories
|
||||
|
||||
To mount a directory, run the VM with the `--dir` argument:
|
||||
@@ -161,3 +212,52 @@ The directory we've mounted above will be accessible from the `/mnt/shared/proje
|
||||
```shell
|
||||
com.apple.virtio-fs.automount /mnt/shared virtiofs rw,relatime 0 0
|
||||
```
|
||||
|
||||
## Working with a Remote OCI Container Registry
|
||||
|
||||
Tart supports interacting with Open Container Initiative (OCI) registries, but only runs images created and pushed by Tart. This means images created for container engines, like Docker, can't be pulled. Instead, create a custom image as documented above.
|
||||
|
||||
For example, let's say you want to push/pull images to an OCI registry hosted at `https://acme.io/`.
|
||||
|
||||
### Registry Authorization
|
||||
|
||||
First, you need to login to `acme.io` with the `tart login` command:
|
||||
|
||||
```bash
|
||||
tart login acme.io
|
||||
```
|
||||
|
||||
If you login to your registry with OAuth, you may need to create an access token to use as the password.
|
||||
Credentials are securely stored in Keychain.
|
||||
|
||||
In addition, Tart supports [Docker credential helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers)
|
||||
if defined in `~/.docker/config.json`.
|
||||
|
||||
Finally, `TART_REGISTRY_USERNAME` and `TART_REGISTRY_PASSWORD` environment variables allow to override authorization
|
||||
for all registries, which might be useful for integrating with your CI's secret management.
|
||||
|
||||
You can also set the `TART_REGISTRY_HOSTNAME` environment variable to apply these overrides only to a specific host.
|
||||
|
||||
### Pushing a Local Image
|
||||
|
||||
Once credentials are saved for `acme.io`, run the following command to push a local images remotely with two tags:
|
||||
|
||||
```bash
|
||||
tart push my-local-vm-name acme.io/remoteorg/name:latest acme.io/remoteorg/name:v1.0.0
|
||||
```
|
||||
|
||||
### Pulling a Remote Image
|
||||
|
||||
You can either pull an image:
|
||||
|
||||
```bash
|
||||
tart pull acme.io/remoteorg/name:latest
|
||||
```
|
||||
|
||||
or create a VM from a remote image:
|
||||
|
||||
```bash
|
||||
tart clone acme.io/remoteorg/name:latest my-local-vm-name
|
||||
```
|
||||
|
||||
If the specified image is not already present, this invocation calls the `tart pull` implicitly before cloning.
|
||||
|
||||
+4
-1
@@ -53,6 +53,9 @@ plugins:
|
||||
debug: true
|
||||
- search
|
||||
- minify
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
'integrations/vm-management.md': quick-start.md
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.admonition
|
||||
@@ -96,7 +99,7 @@ nav:
|
||||
- "GitHub Actions": https://cirrus-runners.app/
|
||||
- "GitLab Runner": integrations/gitlab-runner.md
|
||||
- "Buildkite": integrations/buildkite.md
|
||||
- "Managing VMs": integrations/vm-management.md
|
||||
- "Packer": integrations/packer.md
|
||||
- "Support & Licensing": licensing.md
|
||||
- "Orchestration":
|
||||
- "Quick Start": orchard/quick-start.md
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker run --pull=always --rm -it -p 8000:8000 -v ${PWD}:/docs ghcr.io/cirruslabs/mkdocs-material-insiders:latest build
|
||||
docker run --pull=always --rm -it -p 8000:8000 -v ${PWD}:/docs ghcr.io/squidfunk/mkdocs-material:latest build
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker run --pull=always --rm -it -p 8000:8000 -v ${PWD}:/docs ghcr.io/cirruslabs/mkdocs-material-insiders:latest
|
||||
docker run --pull=always --rm -it -p 8000:8000 -v ${PWD}:/docs ghcr.io/squidfunk/mkdocs-material:latest
|
||||
|
||||
Reference in New Issue
Block a user