From a48bedf90c7ffeb920f1be629a80757892b4e65d Mon Sep 17 00:00:00 2001 From: Ana Maria Martinez Gomez Date: Mon, 27 Jan 2025 11:42:54 +0100 Subject: [PATCH 1/5] [vbox] Rename variables for consistency Rename the following variables for consistency with VBoxManage and the other vbox scripts: -`machine_guid` -> `vm_uuid` (also when used in plural) -`vms_output` -> `vms_info` --- virtualbox/vbox-adapter-check.py | 30 ++++++++++----------- virtualbox/vbox-export-snapshots.py | 14 +++++----- virtualbox/vboxcommon.py | 42 ++++++++++++++--------------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/virtualbox/vbox-adapter-check.py b/virtualbox/vbox-adapter-check.py index dcf0bd4..3f56fdd 100755 --- a/virtualbox/vbox-adapter-check.py +++ b/virtualbox/vbox-adapter-check.py @@ -33,26 +33,26 @@ def get_vm_uuids(dynamic_only): """Gets the machine UUID(s) for a given VM name using 'VBoxManage list vms'.""" - machine_guids = [] + vm_uuids = [] try: # regex VM name and extract the GUID # "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc} - vms_output = run_vboxmanage(["list", "vms"]) + vms_info = run_vboxmanage(["list", "vms"]) pattern = r'"(.*?)" \{(.*?)\}' - matches = re.findall(pattern, vms_output) + matches = re.findall(pattern, vms_info) for match in matches: vm_name = match[0] - machine_guid = match[1] + vm_uuid = match[1] # either get all vms if dynamic_only false, or just the dynamic vms if true if (not dynamic_only) or DYNAMIC_VM_NAME in vm_name: - machine_guids.append((vm_name, machine_guid)) + vm_uuids.append((vm_name, vm_uuid)) except Exception as e: raise Exception(f"Error finding machines UUIDs") from e - return machine_guids + return vm_uuids def change_network_adapters_to_hostonly( - machine_guid, vm_name, hostonly_ifname, do_not_modify + vm_uuid, vm_name, hostonly_ifname, do_not_modify ): """Verify all adapters are in an allowed configuration. Must be poweredoff""" try: @@ -71,7 +71,7 @@ def change_network_adapters_to_hostonly( # nic7="none" # nic8="none" - vminfo = run_vboxmanage(["showvminfo", machine_guid, "--machinereadable"]) + vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) for nic_number, nic_value in re.findall( '^nic(\d+)="(\S+)"', vminfo, flags=re.M ): @@ -87,11 +87,11 @@ def change_network_adapters_to_hostonly( else: message = f"{vm_name} may be connected to the internet on adapter(s): {nic}. The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity. Please double check your VMs settings." # different commands are necessary if the machine is running. - if get_vm_state(machine_guid) == "poweroff": + if get_vm_state(vm_uuid) == "poweroff": run_vboxmanage( [ "modifyvm", - machine_guid, + vm_uuid, f"--{nic}", DISABLED_ADAPTER_TYPE, ] @@ -100,7 +100,7 @@ def change_network_adapters_to_hostonly( run_vboxmanage( [ "controlvm", - machine_guid, + vm_uuid, nic, "hostonly", hostonly_ifname, @@ -164,11 +164,11 @@ def main(argv=None): try: hostonly_ifname = ensure_hostonlyif_exists() - machine_guids = get_vm_uuids(args.dynamic_only) - if len(machine_guids) > 0: - for vm_name, machine_guid in machine_guids: + vm_uuids = get_vm_uuids(args.dynamic_only) + if len(vm_uuids) > 0: + for vm_name, vm_uuid in vm_uuids: change_network_adapters_to_hostonly( - machine_guid, vm_name, hostonly_ifname, args.do_not_modify + vm_uuid, vm_name, hostonly_ifname, args.do_not_modify ) else: print(f"[Warning ⚠️] No VMs found") diff --git a/virtualbox/vbox-export-snapshots.py b/virtualbox/vbox-export-snapshots.py index 44f9604..3355996 100755 --- a/virtualbox/vbox-export-snapshots.py +++ b/virtualbox/vbox-export-snapshots.py @@ -80,24 +80,24 @@ def get_vm_uuid(vm_name): raise Exception(f"Could not find VM '{vm_name}'") from e -def change_network_adapters_to_hostonly(machine_guid): +def change_network_adapters_to_hostonly(vm_uuid): """Changes all active network adapters to Host-Only. Must be poweredoff""" ensure_hostonlyif_exists() try: # disable all the nics to get to a clean state - vminfo = run_vboxmanage(["showvminfo", machine_guid, "--machinereadable"]) + vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) for nic_number, nic_value in re.findall( '^nic(\d+)="(\S+)"', vminfo, flags=re.M ): if nic_value != "none": # Ignore NICs with value "none" - run_vboxmanage(["modifyvm", machine_guid, f"--nic{nic_number}", "none"]) + run_vboxmanage(["modifyvm", vm_uuid, f"--nic{nic_number}", "none"]) print(f"Changed nic{nic_number}") # set first nic to hostonly - run_vboxmanage(["modifyvm", machine_guid, f"--nic1", "hostonly"]) + run_vboxmanage(["modifyvm", vm_uuid, f"--nic1", "hostonly"]) # ensure changes applied - vminfo = run_vboxmanage(["showvminfo", machine_guid, "--machinereadable"]) + vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) for nic_number, nic_value in re.findall( '^nic(\d+)="(\S+)"', vminfo, flags=re.M ): @@ -119,8 +119,8 @@ def change_network_adapters_to_hostonly(machine_guid): raise Exception("Failed to change VM network adapters to hostonly") from e -def restore_snapshot(machine_guid, snapshot_name): - status = run_vboxmanage(["snapshot", machine_guid, "restore", snapshot_name]) +def restore_snapshot(vm_uuid, snapshot_name): + status = run_vboxmanage(["snapshot", vm_uuid, "restore", snapshot_name]) print(f"Restored '{snapshot_name}'") return status diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index f855c76..b473893 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -89,38 +89,38 @@ def ensure_hostonlyif_exists(): raise Exception("Failed to verify host-only interface exists") from e -def get_vm_state(machine_guid): +def get_vm_state(vm_uuid): """Gets the VM state using 'VBoxManage showvminfo'.""" # VMState="poweroff" # VMStateChangeTime="2025-01-02T16:31:51.000000000" - vminfo = run_vboxmanage(["showvminfo", machine_guid, "--machinereadable"]) + vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) for line in vminfo.splitlines(): if line.startswith("VMState"): return line.split("=")[1].strip('"') - raise Exception(f"Could not start VM '{machine_guid}'") + raise Exception(f"Could not start VM '{vm_uuid}'") -def ensure_vm_running(machine_guid): +def ensure_vm_running(vm_uuid): """Checks if the VM is running and starts it if it's not. Waits up to 1 minute for the VM to transition to the 'running' state. """ try: - vm_state = get_vm_state(machine_guid) + vm_state = get_vm_state(vm_uuid) if vm_state != "running": print( - f"VM {machine_guid} is not running (state: {vm_state}). Starting VM..." + f"VM {vm_uuid} is not running (state: {vm_state}). Starting VM..." ) - run_vboxmanage(["startvm", machine_guid, "--type", "gui"]) + run_vboxmanage(["startvm", vm_uuid, "--type", "gui"]) # Wait for VM to start (up to 1 minute) timeout = 60 # seconds check_interval = 5 # seconds start_time = time.time() while time.time() - start_time < timeout: - vm_state = get_vm_state(machine_guid) + vm_state = get_vm_state(vm_uuid) if vm_state == "running": - print(f"VM {machine_guid} started.") + print(f"VM {vm_uuid} started.") time.sleep(5) # wait a bit to be careful and avoid any weird races return print(f"Waiting for VM (state: {vm_state})") @@ -133,40 +133,40 @@ def ensure_vm_running(machine_guid): print("VM is already running.") return except Exception as e: - raise Exception(f"Could not ensure '{machine_guid}' running") from e + raise Exception(f"Could not ensure '{vm_uuid}' running") from e -def ensure_vm_shutdown(machine_guid): +def ensure_vm_shutdown(vm_uuid): """Checks if the VM is running and shuts it down if it is.""" try: - vm_state = get_vm_state(machine_guid) + vm_state = get_vm_state(vm_uuid) if vm_state == "saved": print( - f"VM {machine_guid} is in a saved state. Powering on for a while then shutting down..." + f"VM {vm_uuid} is in a saved state. Powering on for a while then shutting down..." ) - ensure_vm_running(machine_guid) + ensure_vm_running(vm_uuid) time.sleep(120) # 2 minutes to boot up - vm_state = get_vm_state(machine_guid) + vm_state = get_vm_state(vm_uuid) if vm_state != "poweroff": - print(f"VM {machine_guid} is not powered off. Shutting down VM...") - run_vboxmanage(["controlvm", machine_guid, "poweroff"]) + print(f"VM {vm_uuid} is not powered off. Shutting down VM...") + run_vboxmanage(["controlvm", vm_uuid, "poweroff"]) # Wait for VM to shut down (up to 1 minute) timeout = 60 # seconds check_interval = 5 # seconds start_time = time.time() while time.time() - start_time < timeout: - vm_state = get_vm_state(machine_guid) + vm_state = get_vm_state(vm_uuid) if vm_state == "poweroff": - print(f"VM {machine_guid} is shut down (status: {vm_state}).") + print(f"VM {vm_uuid} is shut down (status: {vm_state}).") time.sleep(5) # wait a bit to be careful and avoid any weird races return time.sleep(check_interval) print("Timeout waiting for VM to shut down. Exiting...") raise TimeoutError("VM did not shut down within the timeout period.") else: - print(f"VM {machine_guid} is already shut down (state: {vm_state}).") + print(f"VM {vm_uuid} is already shut down (state: {vm_state}).") return except Exception as e: - raise Exception(f"Could not ensure '{machine_guid}' shutdown") from e + raise Exception(f"Could not ensure '{vm_uuid}' shutdown") from e From f1bc666edd03eed043a8db7657e166f595f38fb3 Mon Sep 17 00:00:00 2001 From: Ana Maria Martinez Gomez Date: Mon, 27 Jan 2025 12:54:48 +0100 Subject: [PATCH 2/5] [vbox] Enhance ensure_vm_ functions Enhance, make consistent and remove duplication in `ensure_vm_running` and `ensure_vm_shutdown` by introducing `wait_until_vm_state`. Remove noisy output and make the output consistent with other vboxcommon functions to make easier to call them from other scripts displaying a nice output. Remove also unneeded use of exceptions re-raising. Remove the waiting time of 2 minutes after calling `ensure_vm_running` from `ensure_vm_shutdown` when the state is `saved, as `ensure_vm_running` already waits for the state to change to `running`. Use `RuntimeError` instead of `TimeoutError`, as if the state didn't change in a minute, an error changing the state may have occurred. --- virtualbox/vboxcommon.py | 102 +++++++++++++++------------------------ 1 file changed, 40 insertions(+), 62 deletions(-) diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index b473893..36d4878 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -101,72 +101,50 @@ def get_vm_state(vm_uuid): raise Exception(f"Could not start VM '{vm_uuid}'") -def ensure_vm_running(vm_uuid): - """Checks if the VM is running and starts it if it's not. - Waits up to 1 minute for the VM to transition to the 'running' state. +def wait_until_vm_state(vm_uuid, target_state): + """Wait for VM state to change. + + Return True if the state changed to the target_stated within one minute. + Return False otherwise. """ - try: + timeout = 60 # seconds + check_interval = 5 # seconds + start_time = time.time() + while time.time() - start_time < timeout: vm_state = get_vm_state(vm_uuid) - if vm_state != "running": - print( - f"VM {vm_uuid} is not running (state: {vm_state}). Starting VM..." - ) - run_vboxmanage(["startvm", vm_uuid, "--type", "gui"]) - - # Wait for VM to start (up to 1 minute) - timeout = 60 # seconds - check_interval = 5 # seconds - start_time = time.time() - while time.time() - start_time < timeout: - vm_state = get_vm_state(vm_uuid) - if vm_state == "running": - print(f"VM {vm_uuid} started.") - time.sleep(5) # wait a bit to be careful and avoid any weird races - return - print(f"Waiting for VM (state: {vm_state})") - time.sleep(check_interval) - print("Timeout waiting for VM to start. Exiting...") - raise TimeoutError( - f"VM did not start within the timeout period {timeout}s." - ) - else: - print("VM is already running.") - return - except Exception as e: - raise Exception(f"Could not ensure '{vm_uuid}' running") from e + if vm_state == target_state: + time.sleep(5) # wait a bit to be careful and avoid any weird races + return True + time.sleep(check_interval) + return False + + +def ensure_vm_running(vm_uuid): + """Start the VM if its state is not 'running'.""" + vm_state = get_vm_state(vm_uuid) + if vm_state == "running": + return + + print(f"VM {vm_uuid} state: {vm_state}. Starting VM...") + run_vboxmanage(["startvm", vm_uuid, "--type", "gui"]) + + if not wait_until_vm_state(vm_uuid, "running"): + raise RuntimeError(f"Unable to start VM {vm_uuid}.") def ensure_vm_shutdown(vm_uuid): - """Checks if the VM is running and shuts it down if it is.""" - try: - vm_state = get_vm_state(vm_uuid) - if vm_state == "saved": - print( - f"VM {vm_uuid} is in a saved state. Powering on for a while then shutting down..." - ) - ensure_vm_running(vm_uuid) - time.sleep(120) # 2 minutes to boot up + """Shut down the VM if its state is not 'poweroff'. If the VM status is 'saved' start it before shutting it down.""" + vm_state = get_vm_state(vm_uuid) + if vm_state == "poweroff": + return + if vm_state == "saved": + ensure_vm_running(vm_uuid) vm_state = get_vm_state(vm_uuid) - if vm_state != "poweroff": - print(f"VM {vm_uuid} is not powered off. Shutting down VM...") - run_vboxmanage(["controlvm", vm_uuid, "poweroff"]) - - # Wait for VM to shut down (up to 1 minute) - timeout = 60 # seconds - check_interval = 5 # seconds - start_time = time.time() - while time.time() - start_time < timeout: - vm_state = get_vm_state(vm_uuid) - if vm_state == "poweroff": - print(f"VM {vm_uuid} is shut down (status: {vm_state}).") - time.sleep(5) # wait a bit to be careful and avoid any weird races - return - time.sleep(check_interval) - print("Timeout waiting for VM to shut down. Exiting...") - raise TimeoutError("VM did not shut down within the timeout period.") - else: - print(f"VM {vm_uuid} is already shut down (state: {vm_state}).") - return - except Exception as e: - raise Exception(f"Could not ensure '{vm_uuid}' shutdown") from e + + print(f"VM {vm_uuid} state: {vm_state}. Shutting down VM...") + run_vboxmanage(["controlvm", vm_uuid, "poweroff"]) + + if not wait_until_vm_state(vm_uuid, "poweroff"): + raise RuntimeError(f"Unable to shutdown VM {vm_uuid}.") + From c34ef71b1b230ae5dbf124ddf8743c2e7f0eb03c Mon Sep 17 00:00:00 2001 From: Ana Maria Martinez Gomez Date: Mon, 27 Jan 2025 13:37:46 +0100 Subject: [PATCH 3/5] [vbox] Enhance ensure_hostonlyif_exists Enhance `ensure_hostonlyif_exists` by: - Introducing `get_hostonlyif_name` to avoid duplication. - Parsing the VBoxManage output with a regular expression for consistency and to improve code readability. - Removing noisy output and making the output consistent with other vboxcommon functions to make easier to call them from other function displaying a nice output. - Removing the unneeded use of exceptions re-raising. - Use concrete exception type (`RuntimeError`). --- virtualbox/vboxcommon.py | 56 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index 36d4878..34a52a6 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -48,45 +48,31 @@ def run_vboxmanage(cmd): return result.stdout +def get_hostonlyif_name(): + """Get the name of the host-only interface. Return None if there is no host-only interface""" + # Example of `VBoxManage list hostonlyifs` relevant output: + # Name: vboxnet0 + hostonlyifs_info = run_vboxmanage(["list", "hostonlyifs"]) + + match = re.search(f"^Name: *(?P\S+)", hostonlyifs_info, flags=re.M) + if match: + return match["hostonlyif_name"] + def ensure_hostonlyif_exists(): - """Gets the name of, or creates a new hostonlyif""" - try: - # Name: vboxnet0 - # GUID: f0000000-dae8-4abf-8000-0a0027000000 - # DHCP: Disabled - # IPAddress: 192.168.56.1 - # NetworkMask: 255.255.255.0 - # IPV6Address: fe80::800:27ff:fe00:0 - # IPV6NetworkMaskPrefixLength: 64 - # HardwareAddress: 0a:00:27:00:00:00 - # MediumType: Ethernet - # Wireless: No - # Status: Up - # VBoxNetworkName: HostInterfaceNetworking-vboxnet0 - - # Find existing hostonlyif - hostonlyifs_output = run_vboxmanage(["list", "hostonlyifs"]) - for line in hostonlyifs_output.splitlines(): - if line.startswith("Name:"): - hostonlyif_name = line.split(":")[1].strip() - print(f"Found existing hostonlyif {hostonlyif_name}") - return hostonlyif_name + """Get the name of the host-only interface. Create the interface if it doesn't exist.""" + hostonlyif_name = get_hostonlyif_name() + if not hostonlyif_name: # No host-only interface found, create one - print("No host-only interface found. Creating one...") run_vboxmanage(["hostonlyif", "create"]) - hostonlyifs_output = run_vboxmanage( - ["list", "hostonlyifs"] - ) # Get the updated list - for line in hostonlyifs_output.splitlines(): - if line.startswith("Name:"): - hostonlyif_name = line.split(":")[1].strip() - print(f"Created hostonlyif {hostonlyif_name}") - return hostonlyif_name - print("Failed to create new hostonlyif. Exiting...") - raise Exception("Failed to create new hostonlyif.") - except Exception as e: - raise Exception("Failed to verify host-only interface exists") from e + + hostonlyif_name = get_hostonlyif_name() + if not hostonlyif_name: + raise RuntimeError("Failed to create new hostonly interface.") + + print(f"VM {vm_uuid} Created hostonly interface: {hostonlyif_name}") + + return hostonlyif_name def get_vm_state(vm_uuid): From 1e82ac5d7ea99b160a669091ef981ae518f87634 Mon Sep 17 00:00:00 2001 From: Ana Maria Martinez Gomez Date: Mon, 27 Jan 2025 17:01:58 +0100 Subject: [PATCH 4/5] [vbox] Enhance get_vm_state Parse the VBoxManage output to get the state with a regular expression for consistency and to improve code readability. Implement other small enhancements in `get_vm_state` for consistency with other functions. --- virtualbox/vboxcommon.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index 34a52a6..2ad8b0b 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -76,15 +76,16 @@ def ensure_hostonlyif_exists(): def get_vm_state(vm_uuid): - """Gets the VM state using 'VBoxManage showvminfo'.""" + """Get the VM state using 'VBoxManage showvminfo'.""" + # Example of `VBoxManage showvminfo --machinereadable` relevant output: # VMState="poweroff" - # VMStateChangeTime="2025-01-02T16:31:51.000000000" + vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) - vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) - for line in vminfo.splitlines(): - if line.startswith("VMState"): - return line.split("=")[1].strip('"') - raise Exception(f"Could not start VM '{vm_uuid}'") + match = re.search(f'^VMState="(?P\S+)"', vm_info, flags=re.M) + if match: + return match["state"] + + raise Exception(f"Unable to get state of VM {vm_uuid}") def wait_until_vm_state(vm_uuid, target_state): From e0ce8a6bfecd2878f39d0ec30f9b54167f66c894 Mon Sep 17 00:00:00 2001 From: Ana Maria Martinez Gomez Date: Mon, 27 Jan 2025 17:34:58 +0100 Subject: [PATCH 5/5] [vbox] Small improvements in vboxcommon Make `run_vboxmanage` consistent with other functions in boxcommon and make black happy with: ``` black --line-length=120 virtualbox/vboxcommon.py ``` --- virtualbox/vboxcommon.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index 2ad8b0b..7491758 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -23,12 +23,14 @@ def format_arg(arg): return f"'{arg}'" return arg + def cmd_to_str(cmd): """Convert a list of string arguments to a string.""" return " ".join(format_arg(arg) for arg in cmd) + def run_vboxmanage(cmd): - """Runs a VBoxManage command and returns the output. + """Run a VBoxManage command and return the output. Args: cmd: list of string arguments to pass to VBoxManage @@ -40,9 +42,9 @@ def run_vboxmanage(cmd): # Use only the first "VBoxManage: error:" line to prevent using the long # VBoxManage help message or noisy information like the details and context. error = f"Command '{cmd_to_str(cmd)}' failed" - stderr_info = re.search("^VBoxManage: error: (.*)", result.stderr, flags=re.M) - if stderr_info: - error += f": {stderr_info.group(1)}" + match = re.search("^VBoxManage: error: (?P.*)", result.stderr, flags=re.M) + if match: + error += f": {match['stderr_info']}" raise RuntimeError(error) return result.stdout @@ -58,6 +60,7 @@ def get_hostonlyif_name(): if match: return match["hostonlyif_name"] + def ensure_hostonlyif_exists(): """Get the name of the host-only interface. Create the interface if it doesn't exist.""" hostonlyif_name = get_hostonlyif_name() @@ -134,4 +137,3 @@ def ensure_vm_shutdown(vm_uuid): if not wait_until_vm_state(vm_uuid, "poweroff"): raise RuntimeError(f"Unable to shutdown VM {vm_uuid}.") -