From e8e446f8c2d8db43390e0f5834803161fe764f84 Mon Sep 17 00:00:00 2001 From: f-trycua Date: Tue, 29 Apr 2025 16:37:33 -0700 Subject: [PATCH] Add lume --storage path --- libs/lume/scripts/install.sh | 79 ++++++++- libs/lume/src/Commands/Create.swift | 2 +- libs/lume/src/Commands/Delete.swift | 2 +- libs/lume/src/Commands/Get.swift | 2 +- libs/lume/src/Commands/List.swift | 11 +- libs/lume/src/Commands/Pull.swift | 2 +- libs/lume/src/Commands/Run.swift | 2 +- libs/lume/src/Commands/Set.swift | 2 +- libs/lume/src/Commands/Stop.swift | 2 +- .../ImageContainerRegistry.swift | 17 +- libs/lume/src/FileSystem/Home.swift | 22 +++ libs/lume/src/FileSystem/VMDirectory.swift | 42 +++-- libs/lume/src/LumeController.swift | 162 +++++++++++++++--- libs/lume/src/Server/Handlers.swift | 4 +- libs/lume/src/Server/Requests.swift | 2 +- libs/lume/src/Server/Server.swift | 6 +- libs/lumier/src/lib/vm.sh | 39 +++-- 17 files changed, 321 insertions(+), 77 deletions(-) diff --git a/libs/lume/scripts/install.sh b/libs/lume/scripts/install.sh index f6313538..d854c0e4 100755 --- a/libs/lume/scripts/install.sh +++ b/libs/lume/scripts/install.sh @@ -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 < "$PLIST_PATH" + + + + + Label + $SERVICE_NAME + ProgramArguments + + $LUME_BIN + serve + + RunAtLoad + + KeepAlive + + WorkingDirectory + $HOME + EnvironmentVariables + + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.local/bin + HOME + $HOME + + StandardOutPath + /tmp/lume_daemon.log + StandardErrorPath + /tmp/lume_daemon.error.log + ProcessType + Interactive + SessionType + Aqua + + +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 \ No newline at end of file +main diff --git a/libs/lume/src/Commands/Create.swift b/libs/lume/src/Commands/Create.swift index b4f02633..db042c69 100644 --- a/libs/lume/src/Commands/Create.swift +++ b/libs/lume/src/Commands/Create.swift @@ -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() { diff --git a/libs/lume/src/Commands/Delete.swift b/libs/lume/src/Commands/Delete.swift index c3cd3653..7d78ca6d 100644 --- a/libs/lume/src/Commands/Delete.swift +++ b/libs/lume/src/Commands/Delete.swift @@ -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() {} diff --git a/libs/lume/src/Commands/Get.swift b/libs/lume/src/Commands/Get.swift index 5ff34113..aad56136 100644 --- a/libs/lume/src/Commands/Get.swift +++ b/libs/lume/src/Commands/Get.swift @@ -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() { diff --git a/libs/lume/src/Commands/List.swift b/libs/lume/src/Commands/List.swift index 6361f899..89a6dc6e 100644 --- a/libs/lume/src/Commands/List.swift +++ b/libs/lume/src/Commands/List.swift @@ -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) } diff --git a/libs/lume/src/Commands/Pull.swift b/libs/lume/src/Commands/Pull.swift index 074e0fac..cd843381 100644 --- a/libs/lume/src/Commands/Pull.swift +++ b/libs/lume/src/Commands/Pull.swift @@ -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() {} diff --git a/libs/lume/src/Commands/Run.swift b/libs/lume/src/Commands/Run.swift index bc659769..273e8ba7 100644 --- a/libs/lume/src/Commands/Run.swift +++ b/libs/lume/src/Commands/Run.swift @@ -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] { diff --git a/libs/lume/src/Commands/Set.swift b/libs/lume/src/Commands/Set.swift index 73bfe0c9..e2420a68 100644 --- a/libs/lume/src/Commands/Set.swift +++ b/libs/lume/src/Commands/Set.swift @@ -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() { diff --git a/libs/lume/src/Commands/Stop.swift b/libs/lume/src/Commands/Stop.swift index 933019e5..3b921114 100644 --- a/libs/lume/src/Commands/Stop.swift +++ b/libs/lume/src/Commands/Stop.swift @@ -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() { diff --git a/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift b/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift index 714cf1cb..a7a68212 100644 --- a/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift +++ b/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift @@ -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( diff --git a/libs/lume/src/FileSystem/Home.swift b/libs/lume/src/FileSystem/Home.swift index b8b4ae54..d83b39b0 100644 --- a/libs/lume/src/FileSystem/Home.swift +++ b/libs/lume/src/FileSystem/Home.swift @@ -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 diff --git a/libs/lume/src/FileSystem/VMDirectory.swift b/libs/lume/src/FileSystem/VMDirectory.swift index a902e34b..3335107d 100644 --- a/libs/lume/src/FileSystem/VMDirectory.swift +++ b/libs/lume/src/FileSystem/VMDirectory.swift @@ -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) } } diff --git a/libs/lume/src/LumeController.swift b/libs/lume/src/LumeController.swift index ecdcec49..f25079ff 100644 --- a/libs/lume/src/LumeController.swift +++ b/libs/lume/src/LumeController.swift @@ -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) } diff --git a/libs/lume/src/Server/Handlers.swift b/libs/lume/src/Server/Handlers.swift index c968359a..bf289350 100644 --- a/libs/lume/src/Server/Handlers.swift +++ b/libs/lume/src/Server/Handlers.swift @@ -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) diff --git a/libs/lume/src/Server/Requests.swift b/libs/lume/src/Server/Requests.swift index da0bf681..5cde19d2 100644 --- a/libs/lume/src/Server/Requests.swift +++ b/libs/lume/src/Server/Requests.swift @@ -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 diff --git a/libs/lume/src/Server/Server.swift b/libs/lume/src/Server/Server.swift index 71db4a75..98ffc588 100644 --- a/libs/lume/src/Server/Server.swift +++ b/libs/lume/src/Server/Server.swift @@ -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", diff --git a/libs/lumier/src/lib/vm.sh b/libs/lumier/src/lib/vm.sh index 9d3dda06..5bcd5d7d 100755 --- a/libs/lumier/src/lib/vm.sh +++ b/libs/lumier/src/lib/vm.sh @@ -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."