Add lume --storage path

This commit is contained in:
f-trycua
2025-04-29 16:37:33 -07:00
parent cf75a7e577
commit e8e446f8c2
17 changed files with 321 additions and 77 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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() {}

View File

@@ -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() {

View File

@@ -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)
}

View File

@@ -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() {}

View File

@@ -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] {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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(

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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",

View File

@@ -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."