mirror of
https://github.com/trycua/computer.git
synced 2026-01-06 21:39:58 -06:00
Add lume --storage path
This commit is contained in:
@@ -12,8 +12,6 @@ GREEN=$(tput setaf 2)
|
||||
BLUE=$(tput setaf 4)
|
||||
YELLOW=$(tput setaf 3)
|
||||
|
||||
|
||||
|
||||
# Default installation directory (user-specific, doesn't require sudo)
|
||||
DEFAULT_INSTALL_DIR="$HOME/.local/bin"
|
||||
INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
|
||||
@@ -204,11 +202,84 @@ main() {
|
||||
create_temp_dir
|
||||
download_release
|
||||
install_binary
|
||||
|
||||
|
||||
echo ""
|
||||
echo "${GREEN}${BOLD}Lume has been successfully installed!${NORMAL}"
|
||||
echo "Run ${BOLD}lume${NORMAL} to get started."
|
||||
|
||||
# --- LaunchAgent setup for lume daemon ---
|
||||
SERVICE_NAME="com.trycua.lume_daemon"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/$SERVICE_NAME.plist"
|
||||
LUME_BIN="$INSTALL_DIR/lume"
|
||||
|
||||
echo ""
|
||||
echo "Setting up LaunchAgent to run lume daemon on login..."
|
||||
|
||||
# Create LaunchAgents directory if it doesn't exist
|
||||
mkdir -p "$HOME/Library/LaunchAgents"
|
||||
|
||||
# Unload existing service if present
|
||||
if [ -f "$PLIST_PATH" ]; then
|
||||
echo "Existing LaunchAgent found. Unloading..."
|
||||
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Create the plist file
|
||||
cat <<EOF > "$PLIST_PATH"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>$SERVICE_NAME</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$LUME_BIN</string>
|
||||
<string>serve</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$HOME</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.local/bin</string>
|
||||
<key>HOME</key>
|
||||
<string>$HOME</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/lume_daemon.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/lume_daemon.error.log</string>
|
||||
<key>ProcessType</key>
|
||||
<string>Interactive</string>
|
||||
<key>SessionType</key>
|
||||
<string>Aqua</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Set permissions
|
||||
chmod 644 "$PLIST_PATH"
|
||||
touch /tmp/lume_daemon.log /tmp/lume_daemon.error.log
|
||||
chmod 644 /tmp/lume_daemon.log /tmp/lume_daemon.error.log
|
||||
|
||||
# Load the LaunchAgent
|
||||
echo "Loading LaunchAgent..."
|
||||
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
||||
launchctl load "$PLIST_PATH"
|
||||
|
||||
echo "${GREEN}Lume daemon LaunchAgent installed and loaded. It will start automatically on login!${NORMAL}"
|
||||
echo "To check status: launchctl list | grep $SERVICE_NAME"
|
||||
echo "To view logs: tail -f /tmp/lume_daemon.log"
|
||||
echo ""
|
||||
echo "To remove the lume daemon service, run:"
|
||||
echo " launchctl unload \"$PLIST_PATH\""
|
||||
echo " rm \"$PLIST_PATH\""
|
||||
}
|
||||
|
||||
# Run the installation
|
||||
main
|
||||
main
|
||||
|
||||
@@ -40,7 +40,7 @@ struct Create: AsyncParsableCommand {
|
||||
)
|
||||
var ipsw: String?
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {
|
||||
|
||||
@@ -12,7 +12,7 @@ struct Delete: AsyncParsableCommand {
|
||||
@Flag(name: .long, help: "Force deletion without confirmation")
|
||||
var force = false
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {}
|
||||
|
||||
@@ -12,7 +12,7 @@ struct Get: AsyncParsableCommand {
|
||||
@Option(name: [.long, .customShort("f")], help: "Output format (json|text)")
|
||||
var format: FormatOption = .text
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {
|
||||
|
||||
@@ -10,15 +10,22 @@ struct List: AsyncParsableCommand {
|
||||
@Option(name: [.long, .customShort("f")], help: "Output format (json|text)")
|
||||
var format: FormatOption = .text
|
||||
|
||||
@Option(name: .long, help: "Filter by storage location name")
|
||||
var storage: String?
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func run() async throws {
|
||||
let manager = LumeController()
|
||||
let vms = try manager.list()
|
||||
let vms = try manager.list(storage: self.storage)
|
||||
if vms.isEmpty && self.format == .text {
|
||||
print("No virtual machines found")
|
||||
if let storageName = self.storage {
|
||||
print("No virtual machines found in storage '\(storageName)'")
|
||||
} else {
|
||||
print("No virtual machines found")
|
||||
}
|
||||
} else {
|
||||
try VMDetailsPrinter.printStatus(vms, format: self.format)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Pull: AsyncParsableCommand {
|
||||
@Option(help: "Organization to pull from. Defaults to trycua")
|
||||
var organization: String = "trycua"
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {}
|
||||
|
||||
@@ -48,7 +48,7 @@ struct Run: AsyncParsableCommand {
|
||||
@Option(help: "For MacOS VMs only, boot into the VM in recovery mode")
|
||||
var recoveryMode: Bool = false
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
private var parsedSharedDirectories: [SharedDirectory] {
|
||||
|
||||
@@ -21,7 +21,7 @@ struct Set: AsyncParsableCommand {
|
||||
@Option(help: "New display resolution in format WIDTHxHEIGHT.")
|
||||
var display: VMDisplayResolution?
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {
|
||||
|
||||
@@ -9,7 +9,7 @@ struct Stop: AsyncParsableCommand {
|
||||
@Argument(help: "Name of the virtual machine", completion: .custom(completeVMName))
|
||||
var name: String
|
||||
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use")
|
||||
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
|
||||
var storage: String?
|
||||
|
||||
init() {
|
||||
|
||||
@@ -643,7 +643,7 @@ class ImageContainerRegistry: @unchecked Sendable {
|
||||
image: String,
|
||||
name: String?,
|
||||
locationName: String? = nil
|
||||
) async throws {
|
||||
) async throws -> VMDirectory {
|
||||
guard !image.isEmpty else {
|
||||
throw ValidationError("Image name cannot be empty")
|
||||
}
|
||||
@@ -652,7 +652,16 @@ class ImageContainerRegistry: @unchecked Sendable {
|
||||
|
||||
// Use provided name or derive from image
|
||||
let vmName = name ?? image.split(separator: ":").first.map(String.init) ?? ""
|
||||
let vmDir = try home.getVMDirectory(vmName, storage: locationName)
|
||||
|
||||
// Determine if locationName is a direct path or a named storage location
|
||||
let vmDir: VMDirectory
|
||||
if let locationName = locationName, locationName.contains("/") || locationName.contains("\\") {
|
||||
// Direct path
|
||||
vmDir = try home.getVMDirectoryFromPath(vmName, storagePath: locationName)
|
||||
} else {
|
||||
// Named storage or default location
|
||||
vmDir = try home.getVMDirectory(vmName, storage: locationName)
|
||||
}
|
||||
|
||||
// Optimize network early in the process
|
||||
optimizeNetworkSettings()
|
||||
@@ -991,6 +1000,7 @@ class ImageContainerRegistry: @unchecked Sendable {
|
||||
Logger.info(
|
||||
"Run 'lume run \(vmName)' to reduce the disk image file size by using macOS sparse file system"
|
||||
)
|
||||
return vmDir
|
||||
}
|
||||
|
||||
// Helper function to clean up a specific cache entry
|
||||
@@ -3024,7 +3034,8 @@ class ImageContainerRegistry: @unchecked Sendable {
|
||||
|
||||
// Replace original with optimized version
|
||||
try FileManager.default.removeItem(at: reassembledFile)
|
||||
try FileManager.default.moveItem(at: optimizedFile, to: reassembledFile)
|
||||
try FileManager.default.moveItem(
|
||||
at: optimizedFile, to: reassembledFile)
|
||||
Logger.info("Using sparse-optimized file for verification")
|
||||
} else {
|
||||
Logger.info(
|
||||
|
||||
@@ -92,6 +92,28 @@ final class Home {
|
||||
let baseDir = Path(location.expandedPath)
|
||||
return VMDirectory(baseDir.directory(name))
|
||||
}
|
||||
|
||||
/// Gets a VM directory from a direct file path
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the VM directory
|
||||
/// - storagePath: Direct file system path where the VM is located
|
||||
/// - Returns: A VMDirectory instance
|
||||
/// - Throws: HomeError if path is invalid
|
||||
func getVMDirectoryFromPath(_ name: String, storagePath: String) throws -> VMDirectory {
|
||||
let baseDir = Path(storagePath)
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if !fileExists(at: storagePath) {
|
||||
Logger.info("Creating storage directory", metadata: ["path": storagePath])
|
||||
try createVMLocation(at: storagePath)
|
||||
} else if !isValidDirectory(at: storagePath) {
|
||||
// Path exists but isn't a valid directory
|
||||
throw HomeError.invalidHomeDirectory
|
||||
}
|
||||
|
||||
return VMDirectory(baseDir.directory(name))
|
||||
}
|
||||
|
||||
/// Returns all initialized VM directories across all locations
|
||||
/// - Returns: An array of VMDirectory instances with location info
|
||||
|
||||
@@ -8,7 +8,7 @@ import Foundation
|
||||
/// - Handling disk operations
|
||||
/// - Managing VM state and locking
|
||||
/// - Providing access to VM-related paths
|
||||
struct VMDirectory {
|
||||
struct VMDirectory: Sendable {
|
||||
// MARK: - Constants
|
||||
|
||||
private enum FileNames {
|
||||
@@ -26,8 +26,6 @@ struct VMDirectory {
|
||||
let configPath: Path
|
||||
let sessionsPath: Path
|
||||
|
||||
private let fileManager: FileManager
|
||||
|
||||
/// The name of the VM directory
|
||||
var name: String { dir.name }
|
||||
|
||||
@@ -36,10 +34,8 @@ struct VMDirectory {
|
||||
/// Creates a new VMDirectory instance
|
||||
/// - Parameters:
|
||||
/// - dir: The base directory path for the VM
|
||||
/// - fileManager: FileManager instance to use for file operations
|
||||
init(_ dir: Path, fileManager: FileManager = .default) {
|
||||
init(_ dir: Path) {
|
||||
self.dir = dir
|
||||
self.fileManager = fileManager
|
||||
self.nvramPath = dir.file(FileNames.nvram)
|
||||
self.diskPath = dir.file(FileNames.disk)
|
||||
self.configPath = dir.file(FileNames.config)
|
||||
@@ -52,7 +48,25 @@ struct VMDirectory {
|
||||
extension VMDirectory {
|
||||
/// Checks if the VM directory is fully initialized with all required files
|
||||
func initialized() -> Bool {
|
||||
configPath.exists() && diskPath.exists() && nvramPath.exists()
|
||||
// Add detailed logging for debugging
|
||||
let configExists = configPath.exists()
|
||||
let diskExists = diskPath.exists()
|
||||
let nvramExists = nvramPath.exists()
|
||||
|
||||
Logger.info(
|
||||
"VM directory initialization check",
|
||||
metadata: [
|
||||
"directory": dir.path,
|
||||
"config_path": configPath.path,
|
||||
"config_exists": "\(configExists)",
|
||||
"disk_path": diskPath.path,
|
||||
"disk_exists": "\(diskExists)",
|
||||
"nvram_path": nvramPath.path,
|
||||
"nvram_exists": "\(nvramExists)"
|
||||
]
|
||||
)
|
||||
|
||||
return configExists && diskExists && nvramExists
|
||||
}
|
||||
|
||||
/// Checks if the VM directory exists
|
||||
@@ -70,7 +84,7 @@ extension VMDirectory {
|
||||
func setDisk(_ size: UInt64) throws {
|
||||
do {
|
||||
if !diskPath.exists() {
|
||||
guard fileManager.createFile(atPath: diskPath.path, contents: nil) else {
|
||||
guard FileManager.default.createFile(atPath: diskPath.path, contents: nil) else {
|
||||
throw VMDirectoryError.fileCreationFailed(diskPath.path)
|
||||
}
|
||||
}
|
||||
@@ -96,7 +110,7 @@ extension VMDirectory {
|
||||
|
||||
do {
|
||||
let data = try encoder.encode(config)
|
||||
guard fileManager.createFile(atPath: configPath.path, contents: data) else {
|
||||
guard FileManager.default.createFile(atPath: configPath.path, contents: data) else {
|
||||
throw VMDirectoryError.fileCreationFailed(configPath.path)
|
||||
}
|
||||
} catch {
|
||||
@@ -108,7 +122,7 @@ extension VMDirectory {
|
||||
/// - Returns: The loaded configuration
|
||||
/// - Throws: VMDirectoryError if the load operation fails
|
||||
func loadConfig() throws -> VMConfig {
|
||||
guard let data = fileManager.contents(atPath: configPath.path) else {
|
||||
guard let data = FileManager.default.contents(atPath: configPath.path) else {
|
||||
throw VMDirectoryError.configNotFound
|
||||
}
|
||||
|
||||
@@ -137,7 +151,7 @@ extension VMDirectory {
|
||||
|
||||
do {
|
||||
let data = try encoder.encode(session)
|
||||
guard fileManager.createFile(atPath: sessionsPath.path, contents: data) else {
|
||||
guard FileManager.default.createFile(atPath: sessionsPath.path, contents: data) else {
|
||||
throw VMDirectoryError.fileCreationFailed(sessionsPath.path)
|
||||
}
|
||||
} catch {
|
||||
@@ -149,7 +163,7 @@ extension VMDirectory {
|
||||
/// - Returns: The loaded VNC session
|
||||
/// - Throws: VMDirectoryError if the load operation fails
|
||||
func loadSession() throws -> VNCSession {
|
||||
guard let data = fileManager.contents(atPath: sessionsPath.path) else {
|
||||
guard let data = FileManager.default.contents(atPath: sessionsPath.path) else {
|
||||
throw VMDirectoryError.sessionNotFound
|
||||
}
|
||||
|
||||
@@ -163,7 +177,7 @@ extension VMDirectory {
|
||||
|
||||
/// Removes the VNC session information from disk
|
||||
func clearSession() {
|
||||
try? fileManager.removeItem(atPath: sessionsPath.path)
|
||||
try? FileManager.default.removeItem(atPath: sessionsPath.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +190,6 @@ extension VMDirectory: CustomStringConvertible {
|
||||
|
||||
extension VMDirectory {
|
||||
func delete() throws {
|
||||
try fileManager.removeItem(atPath: dir.path)
|
||||
try FileManager.default.removeItem(atPath: dir.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +48,72 @@ final class LumeController {
|
||||
|
||||
/// Lists all virtual machines in the system
|
||||
@MainActor
|
||||
public func list() throws -> [VMDetails] {
|
||||
public func list(storage: String? = nil) throws -> [VMDetails] {
|
||||
do {
|
||||
let vmLocations = try home.getAllVMDirectories()
|
||||
let statuses = try vmLocations.map { vmWithLoc in
|
||||
let vm = try self.get(
|
||||
name: vmWithLoc.directory.name, storage: vmWithLoc.locationName)
|
||||
return vm.details
|
||||
if let storage = storage {
|
||||
// If storage is specified, only return VMs from that location
|
||||
if storage.contains("/") || storage.contains("\\") {
|
||||
// Direct path - check if it exists
|
||||
if !FileManager.default.fileExists(atPath: storage) {
|
||||
// Return empty array if the path doesn't exist
|
||||
return []
|
||||
}
|
||||
|
||||
// Try to get all VMs from the specified path
|
||||
// We need to check which subdirectories are valid VM dirs
|
||||
let directoryURL = URL(fileURLWithPath: storage)
|
||||
let contents = try FileManager.default.contentsOfDirectory(
|
||||
at: directoryURL,
|
||||
includingPropertiesForKeys: [.isDirectoryKey],
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
|
||||
let statuses = try contents.compactMap { subdir -> VMDetails? in
|
||||
guard let isDirectory = try subdir.resourceValues(forKeys: [.isDirectoryKey]).isDirectory,
|
||||
isDirectory else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let vmName = subdir.lastPathComponent
|
||||
// Check if it's a valid VM directory
|
||||
let vmDir = try home.getVMDirectoryFromPath(vmName, storagePath: storage)
|
||||
if !vmDir.initialized() {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let vm = try self.get(name: vmName, storage: storage)
|
||||
return vm.details
|
||||
} catch {
|
||||
// Skip invalid VM directories
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
} else {
|
||||
// Named storage
|
||||
let vmsWithLoc = try home.getAllVMDirectories()
|
||||
let statuses = try vmsWithLoc.compactMap { vmWithLoc -> VMDetails? in
|
||||
// Only include VMs from the specified location
|
||||
if vmWithLoc.locationName != storage {
|
||||
return nil
|
||||
}
|
||||
let vm = try self.get(
|
||||
name: vmWithLoc.directory.name, storage: vmWithLoc.locationName)
|
||||
return vm.details
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
} else {
|
||||
// No storage filter - get all VMs
|
||||
let vmsWithLoc = try home.getAllVMDirectories()
|
||||
let statuses = try vmsWithLoc.compactMap { vmWithLoc -> VMDetails? in
|
||||
let vm = try self.get(
|
||||
name: vmWithLoc.directory.name, storage: vmWithLoc.locationName)
|
||||
return vm.details
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
return statuses
|
||||
} catch {
|
||||
Logger.error("Failed to list VMs", metadata: ["error": error.localizedDescription])
|
||||
throw error
|
||||
@@ -133,20 +190,42 @@ final class LumeController {
|
||||
public func get(name: String, storage: String? = nil) throws -> VM {
|
||||
let normalizedName = normalizeVMName(name: name)
|
||||
do {
|
||||
// Try to find the VM and get its actual location
|
||||
let actualLocation = try self.validateVMExists(
|
||||
normalizedName, storage: storage)
|
||||
let vm: VM
|
||||
if let storagePath = storage, storagePath.contains("/") || storagePath.contains("\\") {
|
||||
// Storage is a direct path
|
||||
let vmDir = try home.getVMDirectoryFromPath(normalizedName, storagePath: storagePath)
|
||||
guard vmDir.initialized() else {
|
||||
// Throw a specific error if the directory exists but isn't a valid VM
|
||||
if vmDir.exists() {
|
||||
throw VMError.notInitialized(normalizedName)
|
||||
} else {
|
||||
throw VMError.notFound(normalizedName)
|
||||
}
|
||||
}
|
||||
// Pass the path as the storage context
|
||||
vm = try self.loadVM(vmDir: vmDir, storage: storagePath)
|
||||
} else {
|
||||
// Storage is nil or a named location
|
||||
let actualLocation = try self.validateVMExists(
|
||||
normalizedName, storage: storage)
|
||||
|
||||
// Load the VM from its actual location
|
||||
let vm = try self.loadVM(name: normalizedName, storage: actualLocation)
|
||||
let vmDir = try home.getVMDirectory(normalizedName, storage: actualLocation)
|
||||
// loadVM will re-check initialized, but good practice to keep validateVMExists result.
|
||||
vm = try self.loadVM(vmDir: vmDir, storage: actualLocation)
|
||||
}
|
||||
return vm
|
||||
} catch {
|
||||
Logger.error("Failed to get VM", metadata: ["error": error.localizedDescription])
|
||||
Logger.error(
|
||||
"Failed to get VM",
|
||||
metadata: [
|
||||
"vmName": normalizedName, "storage": storage ?? "default",
|
||||
"error": error.localizedDescription,
|
||||
])
|
||||
// Re-throw the original error to preserve its type
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating the appropriate VM type based on the OS
|
||||
@MainActor
|
||||
public func create(
|
||||
name: String,
|
||||
@@ -488,7 +567,7 @@ final class LumeController {
|
||||
|
||||
let imageContainerRegistry = ImageContainerRegistry(
|
||||
registry: registry, organization: organization)
|
||||
try await imageContainerRegistry.pull(
|
||||
let _ = try await imageContainerRegistry.pull(
|
||||
image: actualImage,
|
||||
name: vmName,
|
||||
locationName: storage)
|
||||
@@ -752,15 +831,17 @@ final class LumeController {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadVM(name: String, storage: String? = nil) throws -> VM {
|
||||
let vmDir = try home.getVMDirectory(name, storage: storage)
|
||||
private func loadVM(vmDir: VMDirectory, storage: String?) throws -> VM {
|
||||
// vmDir is now passed directly
|
||||
guard vmDir.initialized() else {
|
||||
throw VMError.notInitialized(name)
|
||||
throw VMError.notInitialized(vmDir.name) // Use name from vmDir
|
||||
}
|
||||
|
||||
let config: VMConfig = try vmDir.loadConfig()
|
||||
// Pass the provided storage (which could be a path or named location)
|
||||
let vmDirContext = VMDirContext(
|
||||
dir: vmDir, config: config, home: home, storage: storage)
|
||||
dir: vmDir, config: config, home: home, storage: storage
|
||||
)
|
||||
|
||||
let imageLoader =
|
||||
config.os.lowercased() == "macos" ? imageLoaderFactory.createImageLoader() : nil
|
||||
@@ -808,11 +889,22 @@ final class LumeController {
|
||||
public func validateVMExists(_ name: String, storage: String? = nil) throws -> String? {
|
||||
// If location is specified, only check that location
|
||||
if let storage = storage {
|
||||
let vmDir = try home.getVMDirectory(name, storage: storage)
|
||||
guard vmDir.initialized() else {
|
||||
throw VMError.notFound(name)
|
||||
// Check if storage is a path by looking for directory separator
|
||||
if storage.contains("/") || storage.contains("\\") {
|
||||
// Treat as direct path
|
||||
let vmDir = try home.getVMDirectoryFromPath(name, storagePath: storage)
|
||||
guard vmDir.initialized() else {
|
||||
throw VMError.notFound(name)
|
||||
}
|
||||
return storage // Return the path as the location identifier
|
||||
} else {
|
||||
// Treat as named storage
|
||||
let vmDir = try home.getVMDirectory(name, storage: storage)
|
||||
guard vmDir.initialized() else {
|
||||
throw VMError.notFound(name)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
// If no location specified, try to find the VM in any location
|
||||
@@ -846,7 +938,29 @@ final class LumeController {
|
||||
throw ValidationError("Organization cannot be empty")
|
||||
}
|
||||
|
||||
let vmDir = try home.getVMDirectory(name, storage: storage)
|
||||
// Determine if storage is a path or a named storage location
|
||||
let vmDir: VMDirectory
|
||||
if let storage = storage, storage.contains("/") || storage.contains("\\") {
|
||||
// Create the base directory if it doesn't exist
|
||||
if !FileManager.default.fileExists(atPath: storage) {
|
||||
Logger.info("Creating VM storage directory", metadata: ["path": storage])
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: storage,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
} catch {
|
||||
throw HomeError.directoryCreationFailed(path: storage)
|
||||
}
|
||||
}
|
||||
|
||||
// Use getVMDirectoryFromPath for direct paths
|
||||
vmDir = try home.getVMDirectoryFromPath(name, storagePath: storage)
|
||||
} else {
|
||||
// Use getVMDirectory for named storage locations
|
||||
vmDir = try home.getVMDirectory(name, storage: storage)
|
||||
}
|
||||
|
||||
if vmDir.exists() {
|
||||
throw VMError.alreadyExists(name)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import Virtualization
|
||||
extension Server {
|
||||
// MARK: - VM Management Handlers
|
||||
|
||||
func handleListVMs() async throws -> HTTPResponse {
|
||||
func handleListVMs(storage: String? = nil) async throws -> HTTPResponse {
|
||||
do {
|
||||
let vmController = LumeController()
|
||||
let vms = try vmController.list()
|
||||
let vms = try vmController.list(storage: storage)
|
||||
return try .json(vms)
|
||||
} catch {
|
||||
return .badRequest(message: error.localizedDescription)
|
||||
|
||||
@@ -109,7 +109,7 @@ struct PushRequest: Codable {
|
||||
let tags: [String] // List of tags to push
|
||||
var registry: String // Registry URL
|
||||
var organization: String // Organization/user in the registry
|
||||
let storage: String? // Optional VM storage location
|
||||
let storage: String? // Optional VM storage location or direct path
|
||||
var chunkSizeMb: Int // Chunk size
|
||||
// dryRun and reassemble are less common for API, default to false?
|
||||
// verbose is usually handled by server logging
|
||||
|
||||
@@ -79,9 +79,11 @@ final class Server {
|
||||
routes = [
|
||||
Route(
|
||||
method: "GET", path: "/lume/vms",
|
||||
handler: { [weak self] _ in
|
||||
handler: { [weak self] request in
|
||||
guard let self else { throw HTTPError.internalError }
|
||||
return try await self.handleListVMs()
|
||||
// Extract storage from query params if present
|
||||
let storage = self.extractQueryParam(request: request, name: "storage")
|
||||
return try await self.handleListVMs(storage: storage)
|
||||
}),
|
||||
Route(
|
||||
method: "GET", path: "/lume/vms/:name",
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
start_vm() {
|
||||
# Set up dedicated storage for this VM
|
||||
STORAGE_NAME="storage_${VM_NAME}"
|
||||
if [ -n "$HOST_STORAGE_PATH" ]; then
|
||||
lume config storage add "$STORAGE_NAME" "$HOST_STORAGE_PATH" >/dev/null 2>&1 || true
|
||||
# Determine storage path for VM
|
||||
STORAGE_PATH="$HOST_STORAGE_PATH"
|
||||
if [ -z "$STORAGE_PATH" ]; then
|
||||
STORAGE_PATH="storage_${VM_NAME}"
|
||||
fi
|
||||
|
||||
# Check if VM exists and its status using JSON format
|
||||
VM_INFO=$(lume get "$VM_NAME" --storage "$STORAGE_NAME" -f json 2>&1)
|
||||
VM_INFO=$(lume get "$VM_NAME" --storage "$STORAGE_PATH" -f json 2>&1)
|
||||
|
||||
# Check if VM not found error
|
||||
if [[ $VM_INFO == *"Virtual machine not found"* ]]; then
|
||||
IMAGE_NAME="${VERSION##*/}"
|
||||
lume pull "$IMAGE_NAME" "$VM_NAME" --storage "$STORAGE_NAME"
|
||||
lume pull "$IMAGE_NAME" "$VM_NAME" --storage "$STORAGE_PATH"
|
||||
else
|
||||
# Parse the JSON status - check if it contains "status" : "running"
|
||||
if [[ $VM_INFO == *'"status" : "running"'* ]]; then
|
||||
# lume_stop "$VM_NAME" "$STORAGE_NAME"
|
||||
lume stop "$VM_NAME" --storage "$STORAGE_NAME"
|
||||
lume_stop "$VM_NAME" "$STORAGE_PATH"
|
||||
# lume stop "$VM_NAME" --storage "$STORAGE_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set VM parameters
|
||||
lume set "$VM_NAME" --cpu "$CPU_CORES" --memory "${RAM_SIZE}MB" --display "$DISPLAY" --storage "$STORAGE_NAME"
|
||||
lume set "$VM_NAME" --cpu "$CPU_CORES" --memory "${RAM_SIZE}MB" --display "$DISPLAY" --storage "$STORAGE_PATH"
|
||||
|
||||
# Fetch VM configuration
|
||||
CONFIG_JSON=$(lume get "$VM_NAME" --storage "$STORAGE_NAME" -f json)
|
||||
CONFIG_JSON=$(lume get "$VM_NAME" --storage "$STORAGE_PATH" -f json)
|
||||
|
||||
# Setup data directory args if necessary
|
||||
SHARED_DIR_ARGS=""
|
||||
@@ -39,8 +39,8 @@ start_vm() {
|
||||
fi
|
||||
|
||||
# Run VM with VNC and shared directory using curl
|
||||
# lume_run $SHARED_DIR_ARGS --storage "$STORAGE_NAME" "$VM_NAME" &
|
||||
lume run "$VM_NAME" --storage "$STORAGE_NAME" --no-display
|
||||
lume_run $SHARED_DIR_ARGS --storage "$STORAGE_PATH" "$VM_NAME" &
|
||||
# lume run "$VM_NAME" --storage "$STORAGE_PATH" --no-display
|
||||
|
||||
# Wait for VM to be running and VNC URL to be available
|
||||
vm_ip=""
|
||||
@@ -50,7 +50,7 @@ start_vm() {
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
# Get VM info as JSON
|
||||
VM_INFO=$(lume get "$VM_NAME" -f json 2>/dev/null)
|
||||
VM_INFO=$(lume get "$VM_NAME" --storage "$STORAGE_PATH" -f json 2>/dev/null)
|
||||
|
||||
# Check if VM has status 'running'
|
||||
if [[ $VM_INFO == *'"status" : "running"'* ]]; then
|
||||
@@ -71,8 +71,8 @@ start_vm() {
|
||||
|
||||
if [ -z "$vm_ip" ] || [ -z "$vnc_url" ]; then
|
||||
echo "Timed out waiting for VM to start or VNC URL to become available."
|
||||
# lume_stop "$VM_NAME" "$STORAGE_NAME" > /dev/null 2>&1
|
||||
lume stop "$VM_NAME" --storage "$STORAGE_NAME" > /dev/null 2>&1
|
||||
lume_stop "$VM_NAME" "$STORAGE_PATH" > /dev/null 2>&1
|
||||
# lume stop "$VM_NAME" --storage "$STORAGE_PATH" > /dev/null 2>&1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -100,13 +100,16 @@ start_vm() {
|
||||
|
||||
stop_vm() {
|
||||
echo "Stopping VM '$VM_NAME'..."
|
||||
STORAGE_NAME="storage_${VM_NAME}"
|
||||
STORAGE_PATH="$HOST_STORAGE_PATH"
|
||||
if [ -z "$STORAGE_PATH" ]; then
|
||||
STORAGE_PATH="storage_${VM_NAME}"
|
||||
fi
|
||||
# Check if the VM exists and is running (use lume get for speed)
|
||||
VM_INFO=$(lume get "$VM_NAME" --storage "$STORAGE_NAME" -f json 2>/dev/null)
|
||||
VM_INFO=$(lume get "$VM_NAME" --storage "$STORAGE_PATH" -f json 2>/dev/null)
|
||||
if [[ -z "$VM_INFO" || $VM_INFO == *"Virtual machine not found"* ]]; then
|
||||
echo "VM '$VM_NAME' does not exist."
|
||||
elif [[ $VM_INFO == *'"status" : "running"'* ]]; then
|
||||
lume_stop "$VM_NAME" "$STORAGE_NAME"
|
||||
lume_stop "$VM_NAME" "$STORAGE_PATH"
|
||||
echo "VM '$VM_NAME' was running and is now stopped."
|
||||
elif [[ $VM_INFO == *'"status" : "stopped"'* ]]; then
|
||||
echo "VM '$VM_NAME' is already stopped."
|
||||
|
||||
Reference in New Issue
Block a user