mirror of https://github.com/cirruslabs/tart.git
Introduce "tart ip" command to retrieve a VM's IP address (#8)
* Introduce "tart ip" command to retrieve a VM's IP address * VM running hint
This commit is contained in:
parent
2c7217f5a8
commit
7a67deab81
|
|
@ -0,0 +1,116 @@
|
|||
import Foundation
|
||||
import Network
|
||||
import Virtualization
|
||||
|
||||
struct ARPCommandFailedError: Error, CustomStringConvertible {
|
||||
var terminationReason: Process.TerminationReason
|
||||
var terminationStatus: Int32
|
||||
|
||||
var description: String {
|
||||
var reason: String
|
||||
|
||||
switch terminationReason {
|
||||
case .exit:
|
||||
reason = "exit code \(terminationStatus)"
|
||||
case .uncaughtSignal:
|
||||
reason = "uncaught signal"
|
||||
default:
|
||||
reason = "unknown reason"
|
||||
}
|
||||
|
||||
return "arp command failed: \(reason)"
|
||||
}
|
||||
}
|
||||
|
||||
struct ARPCommandYieldedInvalidOutputError: Error, CustomStringConvertible {
|
||||
var explanation: String
|
||||
|
||||
var description: String {
|
||||
"arp command yielded invalid output: \(explanation)"
|
||||
}
|
||||
}
|
||||
|
||||
struct ARPCacheInternalError: Error, CustomStringConvertible {
|
||||
var explanation: String
|
||||
|
||||
var description: String {
|
||||
"ARPCache internal error: \(explanation)"
|
||||
}
|
||||
}
|
||||
|
||||
struct ARPCache {
|
||||
static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
|
||||
let process = Process.init()
|
||||
process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
|
||||
process.arguments = ["-an"]
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
process.standardInput = FileHandle.nullDevice
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
|
||||
throw ARPCommandFailedError(
|
||||
terminationReason: process.terminationReason,
|
||||
terminationStatus: process.terminationStatus)
|
||||
}
|
||||
|
||||
guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
|
||||
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
|
||||
}
|
||||
let lines = String(decoding: rawLines, as: UTF8.self)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.components(separatedBy: "\n")
|
||||
|
||||
// Based on https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/arp.tproj/arp.c.auto.html
|
||||
let regex = try NSRegularExpression(pattern: #"^.* \((?<ip>.*)\) at (?<mac>.*) on (?<interface>.*) .*$"#)
|
||||
|
||||
for line in lines {
|
||||
let nsLineRange = NSRange(line.startIndex..<line.endIndex, in: line)
|
||||
|
||||
guard let match = regex.firstMatch(in: line, range: nsLineRange) else {
|
||||
throw ARPCommandYieldedInvalidOutputError(explanation: "unparseable entry \"\(line)\"")
|
||||
}
|
||||
|
||||
let rawIP = try match.getCaptureGroup(name: "ip", for: line)
|
||||
guard let ip = IPv4Address(rawIP) else {
|
||||
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse IPv4 address \(rawIP)")
|
||||
}
|
||||
|
||||
let rawMAC = try match.getCaptureGroup(name: "mac", for: line)
|
||||
guard let mac = MACAddress(fromString: rawMAC) else {
|
||||
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
|
||||
}
|
||||
|
||||
let interface = try match.getCaptureGroup(name: "interface", for: line)
|
||||
if bridgeOnly && !interface.starts(with: "bridge") {
|
||||
continue
|
||||
}
|
||||
|
||||
if macAddress == mac {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension NSTextCheckingResult {
|
||||
func getCaptureGroup(name: String, for string: String) throws -> String {
|
||||
let nsRange = self.range(withName: name)
|
||||
|
||||
if nsRange.location == NSNotFound {
|
||||
throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
|
||||
}
|
||||
|
||||
guard let range = Range.init(nsRange, in: string) else {
|
||||
throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
|
||||
}
|
||||
|
||||
return String(string[range])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import Foundation
|
||||
|
||||
struct MACAddress: Equatable, CustomStringConvertible {
|
||||
var mac: [UInt8] = Array(repeating: 0, count: 6)
|
||||
|
||||
init?(fromString: String) {
|
||||
let components = fromString.components(separatedBy: ":")
|
||||
|
||||
if components.count != 6 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for (index, component) in components.enumerated() {
|
||||
mac[index] = UInt8(component, radix: 16)!
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import SystemConfiguration
|
||||
|
||||
struct IP: ParsableCommand {
|
||||
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
|
||||
|
||||
@Argument(help: "VM name")
|
||||
var name: String
|
||||
|
||||
func run() throws {
|
||||
Task {
|
||||
do {
|
||||
let vmDir = try VMStorage().read(name)
|
||||
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
let vmMacAddress = MACAddress(fromString: vmConfig.macAddress.string)!
|
||||
|
||||
guard let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) else {
|
||||
print("no IP address found, is your VM running?")
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
||||
print(ip)
|
||||
|
||||
Foundation.exit(0)
|
||||
} catch {
|
||||
print(error)
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
dispatchMain()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,5 @@ import ArgumentParser
|
|||
struct Root: ParsableCommand {
|
||||
static var configuration = CommandConfiguration(
|
||||
commandName: "tart",
|
||||
subcommands: [Create.self, Run.self, List.self, Delete.self])
|
||||
subcommands: [Create.self, Run.self, List.self, IP.self, Delete.self])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
auxStorage: auxStorage,
|
||||
hardwareModel: vmConfig.hardwareModel,
|
||||
cpuCount: vmConfig.cpuCount,
|
||||
memorySize: vmConfig.memorySize
|
||||
memorySize: vmConfig.memorySize,
|
||||
macAddress: vmConfig.macAddress
|
||||
)
|
||||
|
||||
self.virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
|
@ -80,7 +81,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
auxStorage: auxStorage,
|
||||
hardwareModel: requirements.hardwareModel,
|
||||
cpuCount: self.vmConfig.cpuCount,
|
||||
memorySize: self.vmConfig.memorySize
|
||||
memorySize: self.vmConfig.memorySize,
|
||||
macAddress: self.vmConfig.macAddress
|
||||
)
|
||||
self.virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
|
|
@ -116,7 +118,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
auxStorage: VZMacAuxiliaryStorage,
|
||||
hardwareModel: VZMacHardwareModel,
|
||||
cpuCount: Int,
|
||||
memorySize: UInt64
|
||||
memorySize: UInt64,
|
||||
macAddress: VZMACAddress
|
||||
) throws -> VZVirtualMachineConfiguration {
|
||||
let configuration = VZVirtualMachineConfiguration()
|
||||
|
||||
|
|
@ -153,6 +156,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
// Networking
|
||||
let vio = VZVirtioNetworkDeviceConfiguration()
|
||||
vio.attachment = VZNATNetworkDeviceAttachment()
|
||||
vio.macAddress = macAddress
|
||||
configuration.networkDevices = [vio]
|
||||
|
||||
// Storage
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ enum CodingKeys: String, CodingKey {
|
|||
case hardwareModel
|
||||
case cpuCount
|
||||
case memorySize
|
||||
case macAddress
|
||||
}
|
||||
|
||||
struct VMConfig: Encodable, Decodable {
|
||||
|
|
@ -14,12 +15,20 @@ struct VMConfig: Encodable, Decodable {
|
|||
var hardwareModel: VZMacHardwareModel
|
||||
var cpuCount: Int
|
||||
var memorySize: UInt64
|
||||
var macAddress: VZMACAddress
|
||||
|
||||
init(ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(), hardwareModel: VZMacHardwareModel, cpuCount: Int, memorySize: UInt64) {
|
||||
init(
|
||||
ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
|
||||
hardwareModel: VZMacHardwareModel,
|
||||
cpuCount: Int,
|
||||
memorySize: UInt64,
|
||||
macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered()
|
||||
) {
|
||||
self.ecid = ecid
|
||||
self.hardwareModel = hardwareModel
|
||||
self.cpuCount = cpuCount
|
||||
self.memorySize = memorySize
|
||||
self.macAddress = macAddress
|
||||
}
|
||||
|
||||
init(fromURL: URL) throws {
|
||||
|
|
@ -63,6 +72,15 @@ struct VMConfig: Encodable, Decodable {
|
|||
self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
|
||||
|
||||
self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
|
||||
|
||||
let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
|
||||
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .hardwareModel,
|
||||
in: container,
|
||||
debugDescription: "failed to initialize VZMacAddress using the provided value")
|
||||
}
|
||||
self.macAddress = macAddress
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -73,5 +91,6 @@ struct VMConfig: Encodable, Decodable {
|
|||
try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
|
||||
try container.encode(self.cpuCount, forKey: .cpuCount)
|
||||
try container.encode(self.memorySize, forKey: .memorySize)
|
||||
try container.encode(self.macAddress.string, forKey: .macAddress)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
440478FD27B1352C0028EFB8 /* Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440478FC27B1352C0028EFB8 /* Create.swift */; };
|
||||
440478FF27B13D590028EFB8 /* Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440478FE27B13D590028EFB8 /* Run.swift */; };
|
||||
4473E1E527A94E28000850C3 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4473E1E427A94E28000850C3 /* main.swift */; };
|
||||
4484BA6B27BF1F270043A359 /* IP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4484BA6A27BF1F270043A359 /* IP.swift */; };
|
||||
4484BA7027BF1F6D0043A359 /* ARPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4484BA6E27BF1F6D0043A359 /* ARPCache.swift */; };
|
||||
4484BA7127BF1F6D0043A359 /* MACAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4484BA6F27BF1F6D0043A359 /* MACAddress.swift */; };
|
||||
44FDBB3427B4177C005A201B /* VMStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FDBB3327B4177C005A201B /* VMStorage.swift */; };
|
||||
44FDBB4227B43E6D005A201B /* VM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FDBB4127B43E6D005A201B /* VM.swift */; };
|
||||
44FDBB4427B4445E005A201B /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FDBB4327B4445E005A201B /* Root.swift */; };
|
||||
|
|
@ -37,6 +40,9 @@
|
|||
440478FE27B13D590028EFB8 /* Run.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Run.swift; sourceTree = "<group>"; };
|
||||
4473E1E127A94E27000850C3 /* tart */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = tart; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4473E1E427A94E28000850C3 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
4484BA6A27BF1F270043A359 /* IP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IP.swift; sourceTree = "<group>"; };
|
||||
4484BA6E27BF1F6D0043A359 /* ARPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARPCache.swift; sourceTree = "<group>"; };
|
||||
4484BA6F27BF1F6D0043A359 /* MACAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MACAddress.swift; sourceTree = "<group>"; };
|
||||
44FDBB3327B4177C005A201B /* VMStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMStorage.swift; sourceTree = "<group>"; };
|
||||
44FDBB3927B43CCF005A201B /* tart-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "tart-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
44FDBB4127B43E6D005A201B /* VM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VM.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -91,9 +97,19 @@
|
|||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4484BA6D27BF1F6D0043A359 /* ARP */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4484BA6E27BF1F6D0043A359 /* ARPCache.swift */,
|
||||
4484BA6F27BF1F6D0043A359 /* MACAddress.swift */,
|
||||
);
|
||||
path = ARP;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
44FDBB4027B43DCB005A201B /* Commands */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4484BA6A27BF1F270043A359 /* IP.swift */,
|
||||
440478FC27B1352C0028EFB8 /* Create.swift */,
|
||||
440478FE27B13D590028EFB8 /* Run.swift */,
|
||||
44FDBB4327B4445E005A201B /* Root.swift */,
|
||||
|
|
@ -106,6 +122,7 @@
|
|||
44FDBB4F27B6A4B6005A201B /* tart */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4484BA6D27BF1F6D0043A359 /* ARP */,
|
||||
44FDBB4027B43DCB005A201B /* Commands */,
|
||||
4473E1E427A94E28000850C3 /* main.swift */,
|
||||
44FDBB3327B4177C005A201B /* VMStorage.swift */,
|
||||
|
|
@ -214,10 +231,13 @@
|
|||
440478FF27B13D590028EFB8 /* Run.swift in Sources */,
|
||||
4473E1E527A94E28000850C3 /* main.swift in Sources */,
|
||||
44FDBB4827B45EA1005A201B /* Delete.swift in Sources */,
|
||||
4484BA6B27BF1F270043A359 /* IP.swift in Sources */,
|
||||
4484BA7027BF1F6D0043A359 /* ARPCache.swift in Sources */,
|
||||
44FDBB3427B4177C005A201B /* VMStorage.swift in Sources */,
|
||||
44FDBB4627B44B35005A201B /* VMConfig.swift in Sources */,
|
||||
44FDBB4227B43E6D005A201B /* VM.swift in Sources */,
|
||||
44FDBB4C27B69515005A201B /* VMDirectory.swift in Sources */,
|
||||
4484BA7127BF1F6D0043A359 /* MACAddress.swift in Sources */,
|
||||
440478FD27B1352C0028EFB8 /* Create.swift in Sources */,
|
||||
44FDBB4427B4445E005A201B /* Root.swift in Sources */,
|
||||
44FDBB4A27B45F6F005A201B /* List.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in New Issue