From a60389427be696b2fbd9a615b6c6584bdfca0d4c Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 30 May 2015 20:26:38 -0600 Subject: [PATCH] Support for VMware linked clones. --- gns3server/modules/vmware/__init__.py | 122 +++++++++++++++++-------- gns3server/modules/vmware/vmware_vm.py | 96 ++++++++++++++++++- 2 files changed, 180 insertions(+), 38 deletions(-) diff --git a/gns3server/modules/vmware/__init__.py b/gns3server/modules/vmware/__init__.py index 780b7597..f808561e 100644 --- a/gns3server/modules/vmware/__init__.py +++ b/gns3server/modules/vmware/__init__.py @@ -44,6 +44,7 @@ class VMware(BaseManager): def __init__(self): super().__init__() + self._execute_lock = asyncio.Lock() self._vmrun_path = None self._vmnets = [] self._vmnet_start_range = 2 @@ -167,31 +168,32 @@ class VMware(BaseManager): @asyncio.coroutine def execute(self, subcommand, args, timeout=60, host_type=None): - vmrun_path = self.vmrun_path - if not vmrun_path: - vmrun_path = self.find_vmrun() - if host_type is None: - host_type = self.host_type + with (yield from self._execute_lock): + vmrun_path = self.vmrun_path + if not vmrun_path: + vmrun_path = self.find_vmrun() + if host_type is None: + host_type = self.host_type - command = [vmrun_path, "-T", host_type, subcommand] - command.extend(args) - log.debug("Executing vmrun with command: {}".format(command)) - try: - process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - except (OSError, subprocess.SubprocessError) as e: - raise VMwareError("Could not execute vmrun: {}".format(e)) + command = [vmrun_path, "-T", host_type, subcommand] + command.extend(args) + log.debug("Executing vmrun with command: {}".format(command)) + try: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VMwareError("Could not execute vmrun: {}".format(e)) - try: - stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout) - except asyncio.TimeoutError: - raise VMwareError("vmrun has timed out after {} seconds!".format(timeout)) + try: + stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VMwareError("vmrun has timed out after {} seconds!".format(timeout)) - if process.returncode: - # vmrun print errors on stdout - vmrun_error = stdout_data.decode("utf-8", errors="ignore") - raise VMwareError("vmrun has returned an error: {}".format(vmrun_error)) + if process.returncode: + # vmrun print errors on stdout + vmrun_error = stdout_data.decode("utf-8", errors="ignore") + raise VMwareError("vmrun has returned an error: {}".format(vmrun_error)) - return stdout_data.decode("utf-8", errors="ignore").splitlines() + return stdout_data.decode("utf-8", errors="ignore").splitlines() @staticmethod def parse_vmware_file(path): @@ -213,6 +215,20 @@ class VMware(BaseManager): continue return pairs + @staticmethod + def write_vmware_file(path, pairs): + """ + Write a VMware file (excepting VMX file). + + :param path: path to the VMware file + :param pairs: settings to write + """ + + with open(path, "w", encoding="utf-8") as f: + for key, value in pairs.items(): + entry = '{} = "{}"\n'.format(key, value) + f.write(entry) + @staticmethod def write_vmx_file(path, pairs): """ @@ -289,31 +305,63 @@ class VMware(BaseManager): continue return vms + @staticmethod + def get_vmware_inventory_path(): + """ + Returns VMware inventory file path. + + :returns: path to the inventory file + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%APPDATA%\Vmware\Inventory.vmls") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Library/Application\ Support/VMware Fusion/vmInventory") + else: + return os.path.expanduser("~/.vmware/inventory.vmls") + + @staticmethod + def get_vmware_preferences_path(): + """ + Returns VMware preferences file path. + + :returns: path to the preferences file + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%APPDATA%\VMware\preferences.ini") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Library/Preferences/VMware Fusion/preferences") + else: + return os.path.expanduser("~/.vmware/preferences") + + @staticmethod + def get_vmware_default_vm_path(): + """ + Returns VMware default VM directory path. + + :returns: path to the default VM directory + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%USERPROFILE%\Documents\Virtual Machines") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Documents/Virtual Machines.localized") + else: + return os.path.expanduser("~/vmware") + def list_vms(self): """ Gets VMware VM list. """ - if sys.platform.startswith("win"): - inventory_path = os.path.expandvars(r"%APPDATA%\Vmware\Inventory.vmls") - elif sys.platform.startswith("darwin"): - inventory_path = os.path.expanduser("~/Library/Application\ Support/VMware Fusion/vmInventory") - else: - inventory_path = os.path.expanduser("~/.vmware/inventory.vmls") - + inventory_path = self.get_vmware_inventory_path() if os.path.exists(inventory_path): return self._get_vms_from_inventory(inventory_path) else: # VMware player has no inventory file, let's search the default location for VMs. - if sys.platform.startswith("win"): - vmware_preferences_path = os.path.expandvars(r"%APPDATA%\VMware\preferences.ini") - default_vm_path = os.path.expandvars(r"%USERPROFILE%\Documents\Virtual Machines") - elif sys.platform.startswith("darwin"): - vmware_preferences_path = os.path.expanduser("~/Library/Preferences/VMware Fusion/preferences") - default_vm_path = os.path.expanduser("~/Documents/Virtual Machines.localized") - else: - vmware_preferences_path = os.path.expanduser("~/.vmware/preferences") - default_vm_path = os.path.expanduser("~/vmware") + vmware_preferences_path = self.get_vmware_preferences_path() + default_vm_path = self.get_vmware_default_vm_path() if os.path.exists(vmware_preferences_path): # the default vm path may be present in VMware preferences file. diff --git a/gns3server/modules/vmware/vmware_vm.py b/gns3server/modules/vmware/vmware_vm.py index 7dd82bd6..fe2bef48 100644 --- a/gns3server/modules/vmware/vmware_vm.py +++ b/gns3server/modules/vmware/vmware_vm.py @@ -105,6 +105,71 @@ class VMwareVM(BaseVM): log.debug("Control VM '{}' result: {}".format(subcommand, result)) return result + @asyncio.coroutine + def create(self): + + if self._linked_clone and not os.path.exists(os.path.join(self.working_dir, os.path.basename(self._vmx_path))): + # create the base snapshot for linked clones + base_snapshot_name = "GNS3 Linked Base for clones" + vmsd_path = os.path.splitext(self._vmx_path)[0] + ".vmsd" + if not os.path.exists(vmsd_path): + raise VMwareError("{} doesn't not exist".format(vmsd_path)) + try: + vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) + except OSError as e: + raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + gns3_snapshot_exists = False + for value in vmsd_pairs.values(): + if value == base_snapshot_name: + gns3_snapshot_exists = True + break + if not gns3_snapshot_exists: + log.info("Creating snapshot '{}'".format(base_snapshot_name)) + yield from self._control_vm("snapshot", base_snapshot_name) + + # create the linked clone based on the base snapshot + new_vmx_path = os.path.join(self.working_dir, self.name + ".vmx") + yield from self._control_vm("clone", + new_vmx_path, + "linked", + "-snapshot={}".format(base_snapshot_name), + "-cloneName={}".format(self.name)) + + try: + vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) + except OSError as e: + raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + + snapshot_name = None + for name, value in vmsd_pairs.items(): + if value == base_snapshot_name: + snapshot_name = name.split(".", 1)[0] + break + + if snapshot_name is None: + raise VMwareError("Could not find the linked base snapshot in {}".format(vmsd_path)) + + num_clones_entry = "{}.numClones".format(snapshot_name) + if num_clones_entry in vmsd_pairs: + try: + nb_of_clones = int(vmsd_pairs[num_clones_entry]) + except ValueError: + raise VMwareError("Value of {} in {} is not a number".format(num_clones_entry, vmsd_path)) + vmsd_pairs[num_clones_entry] = str(nb_of_clones - 1) + + for clone_nb in range(0, nb_of_clones): + clone_entry = "{}.clone{}".format(snapshot_name, clone_nb) + if clone_entry in vmsd_pairs: + del vmsd_pairs[clone_entry] + + try: + self.manager.write_vmware_file(vmsd_path, vmsd_pairs) + except OSError as e: + raise VMwareError('Could not write VMware VMSD file "{}": {}'.format(vmsd_path, e)) + + # update the VMX file path + self._vmx_path = new_vmx_path + def _get_vmx_setting(self, name, value=None): if name in self._vmx_pairs: @@ -342,7 +407,11 @@ class VMwareVM(BaseVM): self._set_network_options() self._set_serial_console() - self.manager.write_vmx_file(self._vmx_path, self._vmx_pairs) + + try: + self.manager.write_vmx_file(self._vmx_path, self._vmx_pairs) + except OSError as e: + raise VMwareError('Could not write VMware VMX file "{}": {}'.format(self._vmx_path, e)) yield from self._start_ubridge() if self._headless: @@ -465,6 +534,31 @@ class VMwareVM(BaseVM): except VMwareError: pass + if self._linked_clone: + # clean the VMware inventory path from this linked clone + inventory_path = self.manager.get_vmware_inventory_path() + if os.path.exists(inventory_path): + try: + inventory_pairs = self.manager.parse_vmware_file(inventory_path) + except OSError as e: + log.warning('Could not read VMware inventory file "{}": {}'.format(inventory_path, e)) + + vmlist_entry = None + for name, value in inventory_pairs.items(): + if value == self._vmx_path: + vmlist_entry = name.split(".", 1)[0] + break + + if vmlist_entry is not None: + for name in inventory_pairs.keys(): + if name.startswith(vmlist_entry): + del inventory_pairs[name] + + try: + self.manager.write_vmware_file(inventory_path, inventory_pairs) + except OSError as e: + raise VMwareError('Could not write VMware inventory file "{}": {}'.format(inventory_path, e)) + log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) self._closed = True