From 3645bc2f741f757bf5bacb1445a90b519869bc75 Mon Sep 17 00:00:00 2001 From: Thomas Carmet <8408330+tcarmet@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:24:23 +0000 Subject: [PATCH] Functional vm creation --- runner_manager/backend/vsphere.py | 93 ++++++++++++++++++------------ runner_manager/models/backend.py | 4 +- tests/unit/backend/test_vsphere.py | 10 +++- 3 files changed, 68 insertions(+), 39 deletions(-) diff --git a/runner_manager/backend/vsphere.py b/runner_manager/backend/vsphere.py index f6724142..27ed3ff3 100644 --- a/runner_manager/backend/vsphere.py +++ b/runner_manager/backend/vsphere.py @@ -2,12 +2,15 @@ from typing import Literal from com.vmware.vcenter.vm.hardware.boot_client import Device as BootDevice +from com.vmware.vcenter.vm_client import (Hardware, Power) from com.vmware.vcenter.vm.hardware_client import ( Cpu, Disk, Ethernet, Memory, ScsiAddressSpec, + Cdrom, + Boot, ) from com.vmware.vcenter_client import ( VM, @@ -33,31 +36,23 @@ class VsphereBackend(BaseBackend): config: VsphereConfig instance_config: VsphereInstanceConfig - @property - def client(self) -> VsphereClient: - # Create the vSphere client if it wasn't already - # created in the class instance - if not hasattr(self, "_client"): - self._create_client() - return self._client - def _create_client(self): session = Session() session.verify = self.config.verify_ssl - self._client = create_vsphere_client( + return create_vsphere_client( server=self.config.server, username=self.config.username, password=self.config.password, session=session, ) - def get_folder(self, datacenter_name, folder_name): + def get_folder(self, client, datacenter_name, folder_name): """ Returns the identifier of a folder Note: The method assumes that there is only one folder and datacenter with the mentioned names. """ - datacenter = self.get_datacenter(datacenter_name) + datacenter = self.get_datacenter(client, datacenter_name) if not datacenter: print("Datacenter '{}' not found".format(datacenter_name)) return None @@ -68,7 +63,7 @@ def get_folder(self, datacenter_name, folder_name): datacenters=set([datacenter]), ) - folder_summaries = self.client.vcenter.Folder.list(filter_spec) + folder_summaries = client.vcenter.Folder.list(filter_spec) if len(folder_summaries) > 0: folder = folder_summaries[0].folder print("Detected folder '{}' as {}".format(folder_name, folder)) @@ -77,7 +72,7 @@ def get_folder(self, datacenter_name, folder_name): print("Folder '{}' not found".format(folder_name)) return None - def get_datacenter(self, datacenter_name): + def get_datacenter(self, client, datacenter_name): """ Returns the identifier of a datacenter Note: The method assumes only one datacenter with the mentioned name. @@ -85,20 +80,20 @@ def get_datacenter(self, datacenter_name): filter_spec = Datacenter.FilterSpec(names=set([datacenter_name])) - datacenter_summaries = self.client.vcenter.Datacenter.list(filter_spec) + datacenter_summaries = client.vcenter.Datacenter.list(filter_spec) if len(datacenter_summaries) > 0: datacenter = datacenter_summaries[0].datacenter return datacenter else: return None - def get_datastore(self, datacenter_name, datastore_name): + def get_datastore(self, client, datacenter_name, datastore_name): """ Returns the identifier of a datastore Note: The method assumes that there is only one datastore and datacenter with the mentioned names. """ - datacenter = self.get_datacenter(datacenter_name) + datacenter = self.get_datacenter(client, datacenter_name) if not datacenter: print("Datacenter '{}' not found".format(datacenter_name)) return None @@ -107,19 +102,19 @@ def get_datastore(self, datacenter_name, datastore_name): names=set([datastore_name]), datacenters=set([datacenter]) ) - datastore_summaries = self.client.vcenter.Datastore.list(filter_spec) + datastore_summaries = client.vcenter.Datastore.list(filter_spec) if len(datastore_summaries) > 0: datastore = datastore_summaries[0].datastore return datastore else: return None - def get_resource_pool(self, datacenter_name, resource_pool_name=None): + def get_resource_pool(self, client, datacenter_name, resource_pool_name=None): """ Returns the identifier of the resource pool with the given name or the first resource pool in the datacenter if the name is not provided. """ - datacenter = self.get_datacenter(datacenter_name) + datacenter = self.get_datacenter(client, datacenter_name) if not datacenter: print("Datacenter '{}' not found".format(datacenter_name)) return None @@ -129,7 +124,7 @@ def get_resource_pool(self, datacenter_name, resource_pool_name=None): datacenters=set([datacenter]), names=names ) - resource_pool_summaries = self.client.vcenter.ResourcePool.list(filter_spec) + resource_pool_summaries = client.vcenter.ResourcePool.list(filter_spec) if len(resource_pool_summaries) > 0: resource_pool = resource_pool_summaries[0].resource_pool print("Selecting ResourcePool '{}'".format(resource_pool)) @@ -138,19 +133,19 @@ def get_resource_pool(self, datacenter_name, resource_pool_name=None): print("ResourcePool not found in Datacenter '{}'".format(datacenter_name)) return None - def placement_spec(self): + def placement_spec(self, client): """ Returns a VM placement spec for a resourcepool. Ensures that the vm folder and datastore are all in the same datacenter which is specified. """ - resource_pool = self.get_resource_pool(self.instance_config.datacenter) + resource_pool = self.get_resource_pool(client, self.instance_config.datacenter) folder = self.get_folder( - self.instance_config.datacenter, self.instance_config.folder + client, self.instance_config.datacenter, self.instance_config.folder ) datastore = self.get_datastore( - self.instance_config.datacenter, self.instance_config.datastore + client, self.instance_config.datacenter, self.instance_config.datastore ) # Create the vm placement spec with the datastore, resource pool and vm @@ -166,26 +161,26 @@ def placement_spec(self): ) return placement_spec - def get_vm(self, name): + def get_vm(self, client, name): """ Returns the identifier of a VM Note: The method assumes only one VM with the mentioned name. """ filter_spec = VM.FilterSpec(names=set([name])) - vm_summaries = self.client.vcenter.VM.list(filter_spec) + vm_summaries = client.vcenter.VM.list(filter_spec) if len(vm_summaries) > 0: vm = vm_summaries[0].vm return vm else: return None - def get_network_backing(self, portgroup_name, datacenter_name, portgroup_type): + def get_network_backing(self, client, portgroup_name, datacenter_name, portgroup_type): """ Gets a standard portgroup network backing for a given Datacenter Note: The method assumes that there is only one standard portgroup and datacenter with the mentioned names. """ - datacenter = self.get_datacenter(datacenter_name) + datacenter = self.get_datacenter(client, datacenter_name) if not datacenter: print("Datacenter '{}' not found".format(datacenter_name)) return None @@ -195,7 +190,7 @@ def get_network_backing(self, portgroup_name, datacenter_name, portgroup_type): names=set([portgroup_name]), types=set([portgroup_type]), ) - network_summaries = self.client.vcenter.Network.list(filter=filter) + network_summaries = client.vcenter.Network.list(filter=filter) if len(network_summaries) > 0: network = network_summaries[0].network @@ -212,9 +207,11 @@ def get_network_backing(self, portgroup_name, datacenter_name, portgroup_type): return None def create(self, runner: Runner) -> Runner: - placement_spec = self.placement_spec() + client: VsphereClient = self._create_client() + placement_spec = self.placement_spec(client) GiB = 1024 * 1024 * 1024 standard_network = self.get_network_backing( + client, self.instance_config.portgroup, self.instance_config.datacenter, Network.Type.STANDARD_PORTGROUP, @@ -226,9 +223,10 @@ def create(self, runner: Runner) -> Runner: ), ) boot_device_order = [ - BootDevice.EntryCreateSpec(BootDevice.Type.ETHERNET), BootDevice.EntryCreateSpec(BootDevice.Type.DISK), + BootDevice.EntryCreateSpec(BootDevice.Type.ETHERNET), ] + log.info("Creating vm specs...") vm_create_spec = VM.CreateSpec( guest_os=self.instance_config.guest_os, name=runner.name, @@ -236,6 +234,12 @@ def create(self, runner: Runner) -> Runner: cpu=Cpu.UpdateSpec(count=self.instance_config.cpu), memory=Memory.UpdateSpec(size_mib=self.instance_config.memory), disks=[ + Disk.CreateSpec( + backing=Disk.BackingSpec( + type=Disk.BackingType.VMDK_FILE, + vmdk_file=self.instance_config.vmdk_file + ) + ), Disk.CreateSpec( type=Disk.HostBusAdapterType.SCSI, scsi=ScsiAddressSpec(bus=0, unit=0), @@ -245,17 +249,32 @@ def create(self, runner: Runner) -> Runner: ), ], nics=[nic], + boot=Boot.CreateSpec( + type=Boot.Type.BIOS, + delay=0, + enter_setup_mode=False + ), boot_devices=boot_device_order, + ) - vm = self.client.vcenter.VM.create(vm_create_spec) - vm_info = self.client.vcenter.VM.get(vm) + vm = client.vcenter.VM.create(vm_create_spec) + + vm_info = client.vcenter.VM.get(vm) - runner.instance_id = vm_info.vm + runner.instance_id = vm_info.name return super().create(runner) def delete(self, runner: Runner): - vm = self.get_vm(runner.instance_id) + client = self._create_client() + vm = self.get_vm(client, runner.instance_id) if vm: - self.client.vcenter.VM.delete(vm) - return super().delete(runner) + state = client.vcenter.vm.Power.get(vm) + if state == Power.Info(state=Power.State.POWERED_ON): + client.vcenter.vm.Power.stop(vm) + elif state == Power.Info(state=Power.State.SUSPENDED): + client.vcenter.vm.Power.start(vm) + client.vcenter.vm.Power.stop(vm) + log.info(f"Deleting {vm}...") + client.vcenter.VM.delete(vm) + return super().delete(runner) \ No newline at end of file diff --git a/runner_manager/models/backend.py b/runner_manager/models/backend.py index 6037ffc8..0ef7b556 100644 --- a/runner_manager/models/backend.py +++ b/runner_manager/models/backend.py @@ -208,9 +208,11 @@ class VsphereInstanceConfig(InstanceConfig): cpu: int = 1 memory: int = 1024 - guest_os: str = "ubuntu64Guest" + guest_os: str = "OTHER_LINUX_64" disk_size_gb: int = 20 datacenter: str folder: str datastore: str portgroup: str + + vmdk_file: str \ No newline at end of file diff --git a/tests/unit/backend/test_vsphere.py b/tests/unit/backend/test_vsphere.py index 1b1f7844..19bd8659 100644 --- a/tests/unit/backend/test_vsphere.py +++ b/tests/unit/backend/test_vsphere.py @@ -7,6 +7,7 @@ from runner_manager.models.runner import Runner from runner_manager.models.runner_group import RunnerGroup +from vmware.vapi.vsphere.client import VsphereClient, create_vsphere_client @fixture() def vsphere_group(settings) -> RunnerGroup: @@ -22,8 +23,9 @@ def vsphere_group(settings) -> RunnerGroup: instance_config=VsphereInstanceConfig( datacenter=os.environ.get("GOVC_DATACENTER"), datastore=os.environ.get("GOVC_DATASTORE"), - folder="folder", + folder="vm", portgroup="VM Network", + vmdk_file="[datastore2] vmdk/jammy-server-cloudimg-amd64.vmdk", ), ), labels=[ @@ -35,5 +37,11 @@ def vsphere_group(settings) -> RunnerGroup: def test_vsphere_client(vsphere_group: RunnerGroup, runner: Runner): + client: VsphereClient = vsphere_group.backend._create_client() + list = client.vcenter.Folder.list() + runner.instance_id = runner.name + vsphere_group.backend.delete(runner) runner = vsphere_group.backend.create(runner) + # vsphere_group.backend.delete(runner) + assert runner.instance_id is not None