Repository: hashicorp/packer-plugin-vsphere

Issue:
Add support for data sources
#### Community Note

Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request.
Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request.
If you are interested in working on this issue or have submitted a pull request, please leave a comment.

#### Description

Given minimal information to access vCenter, that is, the vCenter host, and some credentials, expose data sources that would allow Packer to discover the resources required by builders.  Examples include:

- hosts
- clusters
- datacenter
- datastore
- datastore items
- network
- content library
- content library items 
- templates

Many vCenter installations will have dedicated resources for automation and, as such, uses one of the following ways to convey that:

- Tag the resource (e.g. Packer)
- Following some naming convention (packer-*)
- Where supported, use custom attributes

By using discovery, we ease maintenance by

- Reducing hardcoded values, be in the packer template, scripts calling packer. In some organizations information disclosure is a concern, and discovery may ease that concern. 
- Reducing the number of variables required
- Reducing the churn involved when things change. For example, I have 40+ packer templates, and when I need to move it to another vCenter, I have to make around 200+ changes, whether it's in packer variable files, the CI/CD system calling it, or the environment.
- Improving reliability

#### Use Case(s)

There are properties in the `vsphere-iso` builder that can be populated:

- host
- folder
- iso_paths
- network
- datastore
- content_library_destination

Each of these resources in vCenter can be tagged, follow a naming convention, and even have custom properties. In many cases, it doesn't actually matter which resources are being used, since Packer will clean up after itself. Instead of hardcoding the values, we can discover the appropriate resources. 

In my case, I have PowerShell code to do this and pass it to Packer:

```
$vCenterHostname    = "$env:PHAKA_VSPHERE_HOSTNAME" 
$vCenterUserName    = "$env:PHAKA_VSPHERE_USERNAME" 
$vCenterPassword    = "$env:PHAKA_VSPHERE_PASSWORD"
$PackerUserName     = "$env:PHAKA_PACKER_USERNAME" 
$PackerPassword     = "$env:PHAKA_PACKER_PASSWORD"  
$SshPassword        = New-Password -Length 32
$SshUsername        = "root"

$Server = Connect-VIServer -Server $vCenterHostname -User $vCenterUserName -Password $vCenterPassword

$VMHost = Get-VMHost -Server $Server -Tag "Packer" 
$VMHostName = $VMHost.Name
 
# Doesn't work when using PowerCLI on macOS
$VMNetwork = Get-VirtualPortGroup -VMHost $VMHost -Tag "Packer"
$VMNetworkName = $VMNetwork.Name

$Datastore = Get-Datastore -Server $Server -Tag "Packer"
$DatastoreName = $Datastore.Name

$ContentLibraryName = "templates"
$FullPath = "Phaka"

# Call packer to create virtual machine
packer build -var "vcenter_folder=$FullPath" `
             -var "vsphere_network=$VMNetworkName" `
             -var "vsphere_host=$VMHostName" `
             -var "vcenter_username=$vCenterUserName" `
             -var "vcenter_password=$vCenterPassword" `
             -var "vcenter_server=$vCenterHostname" `
             -var "vcenter_datastore=$DatastoreName" `
             -var "vcenter_content_library=$ContentLibraryName" `
             -var "create_snapshot=false" `
             -var "ssh_password=$SshPassword" `
             -var "ssh_username=$SshUsername" `
             -var "disk_size=32768" `
             -var "packer_password=$PackerPassword" `
             -var "packer_username=$PackerUserName" `
             "."
```

The ability to use data sources within the packer template to replace the PowerShell script reduces the maintenance of the template and makes it a whole lot more portable between vCenter servers. 

I also have an installation with several hosts managed by vCenter, but which isn't a cluster. In that case, I have much more complex powershell script to get all the hosts, sort them by capacity, and use the one with the most capacity, which is almost always memory.  While most of the hosts are identical in configuration, there are some with SSD storage which may be preferable when it improves the time to build a template.

Consider that I have about 45 packer templates, creating a base template for every supported version of Debian, Ubuntu, Red Hat, CentOS, FreeBSD, OpenBSD, Windows and Solaris, for the 32 and 64 bit architectures where supported. That's a lot of templates to maintain. 

Then I proceed to "specialize" those templates to be a test host, build agent and whatnot. In this case, I find all the templates that meets a certain criteria, for example names that start with`ubuntu-20.04-amd64-*`, and then proceed to sort them descending according to the date they were created, and then use the latest one.  Having a data source to do this, would allow the `vsphere-clone` builder to specialize the latest base template (created earlier). 

I'm finding that I'm passing around a lot of the same information, and that my templates aren't really that portable between vCenter installations.

#### Potential configuration

Since there's several resources involved, I'll focus on finding the datastore: 

1. Finding a datastore that's tagged with `TagA`. If there is more than one, fail. 
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        filters = {
            tags = ["TagA"]
        }
    }
    ```
    
    One can have a filter for `tag="TagA"` which is essentially the same. 

1. Finding a datastore that's tagged with `TagA`. If there is more than one, fail. 
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        filters = {
            tags = ["TagA"]
        }
    }
    ```
    
    One can have a filter for `tag="TagA"` which is essentially the same. 

1. Find the datastore with tag `TagA`, and if there are several tagged, return the one with the most capacity (available storage). 
    
   ```
    data "vsphere-datastore" "basic-datastore" {
         filters = {
            tags = ["TagA"]
        }
        most_capacity = true
    } 
    ```
    
1. Find a datastore named "datastore1"
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        filters = {
            name = "datastore1"
        }
    }
    ```

1. Find a datastore with the name that starts with "datastore", and if many exist, return the one with the most capacity. When you have multiple standalone hosts in vCenter, the datastores of each host are renamed so there's no duplicates. You may end up with `datastore1`, `datastore1 (1)` and `datastore1 (2)`.  
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        filters = {
            name = "datastore*"
        }
        most_capacity = true
    }
    ```

1. Find the datastore with the most capacity in datacenter X
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        datacenter = "X"
        most_capacity = true
    }
    ```

1. Find the datastore with the most capacity on host "X"
    
   ```
    data "vsphere-datastore" "basic-datastore" {
        host = "X"
        most_capacity = true
    }
    ```

Now that we have a datastore, we can complete the template as follows:

```
source "vsphere-iso" "main" {
   # Omitted
   host  = data.vsphere-datastore.basic-datastore.host.id
   datastore  = data.vsphere-datastore.basic-datastore.id
   # Omitted
}
```


Here's another example, suppose I want to find the path of the latest Ubuntu 22.04 ISO stored in a content library tagged with `TagA`. Suppose there's a different process responsible for uploading detecting and uploading new versions of the Ubuntu 22.04 ISOs to the content library. Today there may be a content item for `ubuntu-22.04-amd64`, next quarter they will upload `ubuntu-22.04.1-amd64`, and in six months, it will be `ubuntu-22.04.2-amd64`.  What we really want is to create an Ubuntu 22.04 base image as quickly as we can, and as such, it's beneficial to just get the latest ISO that was uploaded. In packer it could look something like this:

```
data "vsphere-contentlibrary" "main" {
    filter {
        tags = ["TagA"]
    }
}

data "vsphere-contentlibrary-item" "main" {
    name = "ubuntu-22.04*-amd64"
    content_library = data.vsphere-contentlibrary.main.id
    type = "iso"
    most_recent = true
}

source "vsphere-iso" "main" {
   # Omitted
   iso_paths     = [ data.vsphere-contentlibrary-item.main.path ]
   # Omitted
}
```

#### Potential References

- [Terraform vSphere data sources](https://registry.terraform.io/providers/hashicorp/vsphere/latest/docs/data-sources/) for examples. Perhaps packer could use much of that code.
- [Packer Amazon EC2 AMI Data Source](https://www.packer.io/plugins/datasources/amazon/ami)


Hints from issue comments:
@tenthirtyam: I think it would be useful to provide a stack ranked list for what data sources would be the **most** beneficial.

Solution patch:
diff --git a/.web-docs/README.md b/.web-docs/README.md
index 626fb9e..deb9ac7 100644
--- a/.web-docs/README.md
+++ b/.web-docs/README.md
@@ -49,6 +49,11 @@ packer plugins install github.com/hashicorp/vsphere
   This builder deploys and publishes new virtual machine to a vSphere Supervisor cluster using VM
   Service.
 
+#### Data Sources
+
+- [vsphere-virtualmachine](/packer/integrations/hashicorp/vsphere/latest/components/data-source/vsphere-virtualmachine) -
+  This data source returns the name of a virtual machine that matches all defined filters.
+
 #### Post-Processors
 
 - [vsphere](/packer/integrations/hashicorp/vsphere/latest/components/post-processor/vsphere) -
diff --git a/.web-docs/components/data-source/virtualmachine/README.md b/.web-docs/components/data-source/virtualmachine/README.md
new file mode 100644
index 0000000..9e396b8
--- /dev/null
+++ b/.web-docs/components/data-source/virtualmachine/README.md
@@ -0,0 +1,235 @@
+Type: `vsphere-virtualmachine`
+Artifact BuilderId: `vsphere.virtualmachine`
+
+This data source retrieves information about existing virtual machines from vSphere
+and returns the name of a virtual machine that matches all specified filters. This
+virtual machine can be used in the vSphere Clone builder to select a template.
+
+## Configuration Reference
+
+### Filters Configuration
+
+**Optional:**
+
+<!-- Code generated from the comments of the Config struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `name` (string) - Basic filter with glob support (e.g. `ubuntu_basic*`). Defaults to `*`.
+  Using strict globs will not reduce execution time because vSphere API
+  returns the full inventory. But can be used for better readability over
+  regular expressions.
+
+- `name_regex` (string) - Extended name filter with regular expressions support
+  (e.g. `ubuntu[-_]basic[0-9]*`). Default is empty. The match of the
+  regular expression is checked by substring. Use `^` and `$` to define a
+  full string. For example, the `^[^_]+$` filter will search names
+  without any underscores. The expression must use
+  [Go Regex Syntax](https://pkg.go.dev/regexp/syntax).
+
+- `template` (bool) - Filter to return only objects that are virtual machine templates.
+  Defaults to `false` and returns all virtual machines.
+
+- `host` (string) - Filter to search virtual machines only on the specified ESX host.
+
+- `tag` ([]Tag) - Filter to return only the virtual machines that have attached all specified tags.
+  Specify one or more `tag` blocks to define list of tags for the filter.
+  
+  HCL Example:
+  
+  ```hcl
+  	tag {
+  	  category = "team"
+  	  name     = "operations"
+  	}
+  	tag {
+  	  category = "sla"
+  	  name     = "gold"
+  	}
+  ```
+
+- `latest` (bool) - This filter determines how to handle multiple virtual machines that were matched
+  with all previous filters. Virtual machine creation time is being used to find
+  the latest. By default, multiple matching machines results in an error.
+
+<!-- End of code generated from the comments of the Config struct in datasource/virtualmachine/data.go; -->
+
+
+### Tags Filter Configuration
+
+**Required:**
+
+<!-- Code generated from the comments of the Tag struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `name` (string) - Name of the tag added to virtual machine which must pass the `tag`
+  filter.
+
+- `category` (string) - Name of the tag category that contains the tag.
+  
+  -> **Note:** Both `name` and `category` must be specified in the `tag`
+  filter.
+
+<!-- End of code generated from the comments of the Tag struct in datasource/virtualmachine/data.go; -->
+
+
+### Connection Configuration
+
+**Optional:**
+
+<!-- Code generated from the comments of the ConnectConfig struct in builder/vsphere/common/step_connect.go; DO NOT EDIT MANUALLY -->
+
+- `vcenter_server` (string) - The fully qualified domain name or IP address of the vCenter instance
+  instance.
+
+- `username` (string) - The username to authenticate with the vCenter instance.
+
+- `password` (string) - The password to authenticate with the vCenter instance.
+
+- `insecure_connection` (bool) - Do not validate the certificate of the vCenter instance.
+  Defaults to `false`.
+  
+  -> **Note:** This option is beneficial in scenarios where the certificate
+  is self-signed or does not meet standard validation criteria.
+
+- `datacenter` (string) - The name of the datacenter object in the vSphere inventory.
+  
+  -> **Note:** Required if more than one datacenter object exists in the
+  vSphere inventory.
+
+<!-- End of code generated from the comments of the ConnectConfig struct in builder/vsphere/common/step_connect.go; -->
+
+
+## Output
+
+<!-- Code generated from the comments of the DatasourceOutput struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `vm_name` (string) - Name of the found virtual machine.
+
+<!-- End of code generated from the comments of the DatasourceOutput struct in datasource/virtualmachine/data.go; -->
+
+
+## Example Usage
+
+### Filter by Tags and Get Latest
+
+This example demonstrates how to connect to vSphere cluster and search for the latest
+virtual machine that matches the specified tag filters.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  latest         = true
+
+  tag {
+    category = "team"
+    name     = "operations"
+  }
+
+  tag {
+    category = "sla"
+    name     = "gold"
+  }
+}
+
+locals {
+  vm_name = data.vsphere-virtualmachine.example.vm_name
+}
+
+source "null" "example" {
+    communicator = "none"
+}
+
+build {
+  sources = [
+    "source.null.example"
+  ]
+
+  provisioner "shell-local" {
+    inline = [
+      "echo vm_name: ${local.vm_name}",
+    ]
+  }
+}
+```
+
+### Filter by Name with Glob Pattern
+
+This example shows how to use the `name` filter with glob patterns to find virtual
+machines matching a specific naming convention.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name           = "linux-debian-13-*"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter by Name with Regular Expression
+
+This example demonstrates using the `name_regex` filter for more complex pattern
+matching with regular expressions.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name_regex     = "^linux-debian-[0-9]+-.+$"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter for VM Templates Only
+
+This example shows how to use the `template` filter to search only for virtual
+machine templates.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name           = "linux-debian-13-*"
+  template       = true
+}
+
+locals {
+  result_template = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter by ESX Host
+
+This example demonstrates filtering virtual machines by a specific ESX host,
+combined with other filters.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  host           = "esxi-01.example.com"
+  name           = "linux-debian-13-*"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
diff --git a/.web-docs/metadata.hcl b/.web-docs/metadata.hcl
index 017cc2a..1a7e558 100644
--- a/.web-docs/metadata.hcl
+++ b/.web-docs/metadata.hcl
@@ -33,4 +33,9 @@ integration {
     name = "vSphere Template"
     slug = "vsphere-template"
   }
+  component {
+    type = "data-source"
+    name = "vSphere Virtual Machine"
+    slug = "vsphere-virtualmachine"
+  }
 }
diff --git a/builder/vsphere/common/step_connect.go b/builder/vsphere/common/step_connect.go
index e35c6b2..1401b71 100644
--- a/builder/vsphere/common/step_connect.go
+++ b/builder/vsphere/common/step_connect.go
@@ -59,6 +59,8 @@ type StepConnect struct {
 }
 
 func (s *StepConnect) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+	// Convert common.ConnectConfig to driver.ConnectConfig
+	// Both structs have identical fields to avoid import cycles
 	d, err := driver.NewDriver(&driver.ConnectConfig{
 		VCenterServer:      s.Config.VCenterServer,
 		Username:           s.Config.Username,
diff --git a/builder/vsphere/driver/cluster.go b/builder/vsphere/driver/cluster.go
index 709b752..d3da6f8 100644
--- a/builder/vsphere/driver/cluster.go
+++ b/builder/vsphere/driver/cluster.go
@@ -14,7 +14,7 @@ type Cluster struct {
 // Returns a Cluster object or an error if not found or if the retrieval
 // process fails.
 func (d *VCenterDriver) FindCluster(name string) (*Cluster, error) {
-	c, err := d.finder.ClusterComputeResource(d.ctx, name)
+	c, err := d.Finder.ClusterComputeResource(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
diff --git a/builder/vsphere/driver/datastore.go b/builder/vsphere/driver/datastore.go
index 0e2e676..07b9610 100644
--- a/builder/vsphere/driver/datastore.go
+++ b/builder/vsphere/driver/datastore.go
@@ -37,7 +37,7 @@ type DatastoreDriver struct {
 // NewDatastore creates a new Datastore object.
 func (d *VCenterDriver) NewDatastore(ref *types.ManagedObjectReference) Datastore {
 	return &DatastoreDriver{
-		ds:     object.NewDatastore(d.client.Client, *ref),
+		ds:     object.NewDatastore(d.Client.Client, *ref),
 		driver: d,
 	}
 }
@@ -68,7 +68,7 @@ func (d *VCenterDriver) FindDatastore(name string, host string) (Datastore, erro
 		name = inf.Name
 	}
 
-	ds, err := d.finder.Datastore(d.ctx, name)
+	ds, err := d.Finder.Datastore(d.Ctx, name)
 	if err != nil {
 		return nil, fmt.Errorf("error finding datastore with name %s: %s", name, err)
 	}
@@ -87,10 +87,10 @@ func (d *VCenterDriver) GetDatastoreName(id string) (string, error) {
 		Type:  "Datastore",
 		Value: id,
 	}
-	pc := property.DefaultCollector(d.vimClient)
+	pc := property.DefaultCollector(d.VimClient)
 	var me mo.ManagedEntity
 
-	err := pc.RetrieveOne(d.ctx, obj, []string{"name"}, &me)
+	err := pc.RetrieveOne(d.Ctx, obj, []string{"name"}, &me)
 	if err != nil {
 		return id, err
 	}
@@ -108,7 +108,7 @@ func (ds *DatastoreDriver) Info(params ...string) (*mo.Datastore, error) {
 		p = params
 	}
 	var info mo.Datastore
-	err := ds.ds.Properties(ds.driver.ctx, ds.ds.Reference(), p, &info)
+	err := ds.ds.Properties(ds.driver.Ctx, ds.ds.Reference(), p, &info)
 	if err != nil {
 		return nil, err
 	}
@@ -116,14 +116,14 @@ func (ds *DatastoreDriver) Info(params ...string) (*mo.Datastore, error) {
 }
 
 func (ds *DatastoreDriver) DirExists(filepath string) bool {
-	_, err := ds.ds.Stat(ds.driver.ctx, filepath)
+	_, err := ds.ds.Stat(ds.driver.Ctx, filepath)
 	var notFoundError *object.DatastoreNoSuchDirectoryError
 	return !errors.As(err, &notFoundError)
 }
 
 // FileExists checks if a file exists in a datastore.
 func (ds *DatastoreDriver) FileExists(path string) bool {
-	_, err := ds.ds.Stat(ds.driver.ctx, path)
+	_, err := ds.ds.Stat(ds.driver.Ctx, path)
 	return err == nil
 }
 
@@ -145,9 +145,9 @@ func (ds *DatastoreDriver) ResolvePath(path string) string {
 // GetDatastoreFilePath retrieves the full path of a file in a specified datastore directory by its datastore ID and name.
 func (d *VCenterDriver) GetDatastoreFilePath(datastoreID, dir, filename string) (string, error) {
 	ref := types.ManagedObjectReference{Type: "Datastore", Value: datastoreID}
-	ds := object.NewDatastore(d.vimClient, ref)
+	ds := object.NewDatastore(d.VimClient, ref)
 
-	b, err := ds.Browser(d.ctx)
+	b, err := ds.Browser(d.Ctx)
 	if err != nil {
 		return filename, err
 	}
@@ -157,12 +157,12 @@ func (d *VCenterDriver) GetDatastoreFilePath(datastoreID, dir, filename string)
 		MatchPattern: []string{pat},
 	}
 
-	task, err := b.SearchDatastore(d.ctx, dir, &spec)
+	task, err := b.SearchDatastore(d.Ctx, dir, &spec)
 	if err != nil {
 		return filename, err
 	}
 
-	info, err := task.WaitForResult(d.ctx, nil)
+	info, err := task.WaitForResult(d.Ctx, nil)
 	if err != nil {
 		return filename, err
 	}
@@ -182,7 +182,7 @@ func (d *VCenterDriver) GetDatastoreFilePath(datastoreID, dir, filename string)
 // in the datastore, with optional host context.
 func (ds *DatastoreDriver) UploadFile(src, dst, host string, setHost bool) error {
 	p := soap.DefaultUpload
-	ctx := ds.driver.ctx
+	ctx := ds.driver.Ctx
 
 	if setHost && host != "" {
 		h, err := ds.driver.FindHost(host)
@@ -197,22 +197,22 @@ func (ds *DatastoreDriver) UploadFile(src, dst, host string, setHost bool) error
 
 // Delete deletes a file from a datastore by a path.
 func (ds *DatastoreDriver) Delete(path string) error {
-	dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
+	dc, err := ds.driver.Finder.Datacenter(ds.driver.Ctx, ds.ds.DatacenterPath)
 	if err != nil {
 		return err
 	}
 	fm := ds.ds.NewFileManager(dc, false)
-	return fm.Delete(ds.driver.ctx, path)
+	return fm.Delete(ds.driver.Ctx, path)
 }
 
 // MakeDirectory creates a directory in a datastore by a path.
 func (ds *DatastoreDriver) MakeDirectory(path string) error {
-	dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
+	dc, err := ds.driver.Finder.Datacenter(ds.driver.Ctx, ds.ds.DatacenterPath)
 	if err != nil {
 		return err
 	}
 	fm := ds.ds.NewFileManager(dc, false)
-	return fm.FileManager.MakeDirectory(ds.driver.ctx, path, dc, true)
+	return fm.FileManager.MakeDirectory(ds.driver.Ctx, path, dc, true)
 }
 
 // RemoveDatastorePrefix removes the datastore prefix from a path.
diff --git a/builder/vsphere/driver/driver.go b/builder/vsphere/driver/driver.go
index 4184c76..465eb97 100644
--- a/builder/vsphere/driver/driver.go
+++ b/builder/vsphere/driver/driver.go
@@ -51,29 +51,29 @@ type Driver interface {
 }
 
 type VCenterDriver struct {
-	// context that controls the authenticated sessions used to run the VM commands
-	ctx        context.Context
-	client     *govmomi.Client
-	vimClient  *vim25.Client
-	restClient *RestClient
-	finder     *find.Finder
-	datacenter *object.Datacenter
+	Ctx        context.Context
+	Client     *govmomi.Client
+	VimClient  *vim25.Client
+	RestClient *RestClient
+	Finder     *find.Finder
+	Datacenter *object.Datacenter
 }
 
 func NewVCenterDriver(ctx context.Context, client *govmomi.Client, vimClient *vim25.Client, user *url.Userinfo, finder *find.Finder, datacenter *object.Datacenter) *VCenterDriver {
 	return &VCenterDriver{
-		ctx:       ctx,
-		client:    client,
-		vimClient: vimClient,
-		restClient: &RestClient{
+		Ctx:       ctx,
+		Client:    client,
+		VimClient: vimClient,
+		RestClient: &RestClient{
 			client:      rest.NewClient(vimClient),
 			credentials: user,
 		},
-		datacenter: datacenter,
-		finder:     finder,
+		Datacenter: datacenter,
+		Finder:     finder,
 	}
 }
 
+// ConnectConfig contains the configuration for connecting to vCenter.
 type ConnectConfig struct {
 	VCenterServer      string
 	Username           string
@@ -117,24 +117,24 @@ func NewDriver(config *ConnectConfig) (Driver, error) {
 	finder.SetDatacenter(datacenter)
 
 	d := &VCenterDriver{
-		ctx:       ctx,
-		client:    client,
-		vimClient: vimClient,
-		restClient: &RestClient{
+		Ctx:       ctx,
+		Client:    client,
+		VimClient: vimClient,
+		RestClient: &RestClient{
 			client:      rest.NewClient(vimClient),
 			credentials: credentials,
 		},
-		datacenter: datacenter,
-		finder:     finder,
+		Datacenter: datacenter,
+		Finder:     finder,
 	}
 	return d, nil
 }
 
 func (d *VCenterDriver) Cleanup() (error, error) {
-	return d.restClient.client.Logout(d.ctx), d.client.SessionManager.Logout(d.ctx)
+	return d.RestClient.client.Logout(d.Ctx), d.Client.SessionManager.Logout(d.Ctx)
 }
 
-// RestClient manages RESTful interactions with vCenter, handling client initialization and credential storage.
+// RestClient manages RESTful interactions with vCenter.
 type RestClient struct {
 	client      *rest.Client
 	credentials *url.Userinfo
@@ -147,3 +147,8 @@ func (r *RestClient) Login(ctx context.Context) error {
 func (r *RestClient) Logout(ctx context.Context) error {
 	return r.client.Logout(ctx)
 }
+
+// Client returns the underlying rest.Client for direct API access.
+func (r *RestClient) Client() *rest.Client {
+	return r.client
+}
diff --git a/builder/vsphere/driver/folder.go b/builder/vsphere/driver/folder.go
index 2c93c2e..e80f6fd 100644
--- a/builder/vsphere/driver/folder.go
+++ b/builder/vsphere/driver/folder.go
@@ -22,7 +22,7 @@ type Folder struct {
 
 func (d *VCenterDriver) NewFolder(ref *types.ManagedObjectReference) *Folder {
 	return &Folder{
-		folder: object.NewFolder(d.client.Client, *ref),
+		folder: object.NewFolder(d.Client.Client, *ref),
 		driver: d,
 	}
 }
@@ -34,17 +34,17 @@ func (d *VCenterDriver) FindFolder(name string) (*Folder, error) {
 	if name != "" {
 		// If the folder does not exist, create it.
 		parent := ""
-		parentFolder, err := d.finder.Folder(d.ctx, path.Join(d.datacenter.InventoryPath, "vm"))
+		parentFolder, err := d.Finder.Folder(d.Ctx, path.Join(d.Datacenter.InventoryPath, "vm"))
 		if err != nil {
 			return nil, err
 		}
 		folders := strings.Split(name, "/")
 		for _, folder := range folders {
 			parent = path.Join(parent, folder)
-			f, err := d.finder.Folder(d.ctx, path.Join(d.datacenter.InventoryPath, "vm", parent))
+			f, err := d.Finder.Folder(d.Ctx, path.Join(d.Datacenter.InventoryPath, "vm", parent))
 			var notFoundError *find.NotFoundError
 			if errors.As(err, &notFoundError) {
-				f, err = parentFolder.CreateFolder(d.ctx, folder)
+				f, err = parentFolder.CreateFolder(d.Ctx, folder)
 			}
 			if err != nil {
 				return nil, err
@@ -53,7 +53,7 @@ func (d *VCenterDriver) FindFolder(name string) (*Folder, error) {
 		}
 	}
 
-	f, err := d.finder.Folder(d.ctx, path.Join(d.datacenter.InventoryPath, "vm", name))
+	f, err := d.Finder.Folder(d.Ctx, path.Join(d.Datacenter.InventoryPath, "vm", name))
 	if err != nil {
 		return nil, err
 	}
@@ -74,7 +74,7 @@ func (f *Folder) Info(params ...string) (*mo.Folder, error) {
 		p = params
 	}
 	var info mo.Folder
-	err := f.folder.Properties(f.driver.ctx, f.folder.Reference(), p, &info)
+	err := f.folder.Properties(f.driver.Ctx, f.folder.Reference(), p, &info)
 	if err != nil {
 		return nil, err
 	}
diff --git a/builder/vsphere/driver/host.go b/builder/vsphere/driver/host.go
index 3c4728f..62df39b 100644
--- a/builder/vsphere/driver/host.go
+++ b/builder/vsphere/driver/host.go
@@ -18,7 +18,7 @@ type Host struct {
 // ManagedObjectReference and the VCenterDriver instance.
 func (d *VCenterDriver) NewHost(ref *types.ManagedObjectReference) *Host {
 	return &Host{
-		host:   object.NewHostSystem(d.client.Client, *ref),
+		host:   object.NewHostSystem(d.Client.Client, *ref),
 		driver: d,
 	}
 }
@@ -26,7 +26,7 @@ func (d *VCenterDriver) NewHost(ref *types.ManagedObjectReference) *Host {
 // FindHost locates a host within the vCenter environment by its name. Returns
 // a Host object or an error if not found or if the retrieval process fails.
 func (d *VCenterDriver) FindHost(name string) (*Host, error) {
-	h, err := d.finder.HostSystem(d.ctx, name)
+	h, err := d.Finder.HostSystem(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -46,7 +46,7 @@ func (h *Host) Info(params ...string) (*mo.HostSystem, error) {
 		p = params
 	}
 	var info mo.HostSystem
-	err := h.host.Properties(h.driver.ctx, h.host.Reference(), p, &info)
+	err := h.host.Properties(h.driver.Ctx, h.host.Reference(), p, &info)
 	if err != nil {
 		return nil, err
 	}
diff --git a/builder/vsphere/driver/library.go b/builder/vsphere/driver/library.go
index 4661fa0..fc7dc18 100644
--- a/builder/vsphere/driver/library.go
+++ b/builder/vsphere/driver/library.go
@@ -20,8 +20,8 @@ type Library struct {
 // FindContentLibraryByName retrieves a content library by its name. Returns a
 // Library object or an error if the library is not found.
 func (d *VCenterDriver) FindContentLibraryByName(name string) (*Library, error) {
-	lm := library.NewManager(d.restClient.client)
-	l, err := lm.GetLibraryByName(d.ctx, name)
+	lm := library.NewManager(d.RestClient.client)
+	l, err := lm.GetLibraryByName(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -35,8 +35,8 @@ func (d *VCenterDriver) FindContentLibraryByName(name string) (*Library, error)
 // the specified library ID.  Returns the library item if found or an error if
 // the item is not found or the retrieval process fails.
 func (d *VCenterDriver) FindContentLibraryItem(libraryId string, name string) (*library.Item, error) {
-	lm := library.NewManager(d.restClient.client)
-	items, err := lm.GetLibraryItems(d.ctx, libraryId)
+	lm := library.NewManager(d.RestClient.client)
+	items, err := lm.GetLibraryItems(d.Ctx, libraryId)
 	if err != nil {
 		return nil, err
 	}
@@ -66,7 +66,7 @@ func (d *VCenterDriver) FindContentLibraryItemUUID(libraryId string, name string
 // not identified as a content library path or if the retrieval process fails.
 func (d *VCenterDriver) FindContentLibraryFileDatastorePath(isoPath string) (string, error) {
 	log.Printf("Check if ISO path is a Content Library path")
-	err := d.restClient.Login(d.ctx)
+	err := d.RestClient.Login(d.Ctx)
 	if err != nil {
 		log.Printf("vCenter client not available. ISO path not identified as a Content Library path")
 		return isoPath, err
@@ -107,20 +107,20 @@ func (d *VCenterDriver) FindContentLibraryFileDatastorePath(isoPath string) (str
 		return isoPath, err
 	}
 
-	_ = d.restClient.Logout(d.ctx)
+	_ = d.RestClient.Logout(d.Ctx)
 	return path.Join(libItemDir, isoFilePath), nil
 }
 
 // UpdateContentLibraryItem updates the metadata of a content library item,
 // such as its name and description. Returns an error if the update fails.
 func (d *VCenterDriver) UpdateContentLibraryItem(item *library.Item, name string, description string) error {
-	lm := library.NewManager(d.restClient.client)
+	lm := library.NewManager(d.RestClient.client)
 	item.Patch(&library.Item{
 		ID:          item.ID,
 		Name:        name,
 		Description: &description,
 	})
-	return lm.UpdateLibraryItem(d.ctx, item)
+	return lm.UpdateLibraryItem(d.Ctx, item)
 }
 
 type LibraryFilePath struct {
diff --git a/builder/vsphere/driver/network.go b/builder/vsphere/driver/network.go
index 5b91fc1..566f79a 100644
--- a/builder/vsphere/driver/network.go
+++ b/builder/vsphere/driver/network.go
@@ -20,7 +20,7 @@ type Network struct {
 // ManagedObjectReference.
 func (d *VCenterDriver) NewNetwork(ref *types.ManagedObjectReference) *Network {
 	return &Network{
-		network: object.NewNetwork(d.client.Client, *ref),
+		network: object.NewNetwork(d.Client.Client, *ref),
 		driver:  d,
 	}
 }
@@ -28,7 +28,7 @@ func (d *VCenterDriver) NewNetwork(ref *types.ManagedObjectReference) *Network {
 // FindNetwork locates a network by its name within the vCenter context.
 // Returns a Network object or an error if the network is not found.
 func (d *VCenterDriver) FindNetwork(name string) (*Network, error) {
-	n, err := d.finder.Network(d.ctx, name)
+	n, err := d.Finder.Network(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -41,7 +41,7 @@ func (d *VCenterDriver) FindNetwork(name string) (*Network, error) {
 // FindNetworks retrieves a list of networks in the vCenter matching the
 // provided name and returns them as Network objects.
 func (d *VCenterDriver) FindNetworks(name string) ([]*Network, error) {
-	ns, err := d.finder.NetworkList(d.ctx, name)
+	ns, err := d.Finder.NetworkList(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -72,7 +72,7 @@ func (n *Network) Info(params ...string) (*mo.Network, error) {
 		return nil, fmt.Errorf("unexpected %t network object type", n.network)
 	}
 
-	err := network.Properties(n.driver.ctx, network.Reference(), p, &info)
+	err := network.Properties(n.driver.Ctx, network.Reference(), p, &info)
 	if err != nil {
 		return nil, err
 	}
diff --git a/builder/vsphere/driver/resource_pool.go b/builder/vsphere/driver/resource_pool.go
index 6b5fec5..8708f11 100644
--- a/builder/vsphere/driver/resource_pool.go
+++ b/builder/vsphere/driver/resource_pool.go
@@ -23,7 +23,7 @@ type ResourcePool struct {
 // provided ManagedObjectReference.
 func (d *VCenterDriver) NewResourcePool(ref *types.ManagedObjectReference) *ResourcePool {
 	return &ResourcePool{
-		pool:   object.NewResourcePool(d.client.Client, *ref),
+		pool:   object.NewResourcePool(d.Client.Client, *ref),
 		driver: d,
 	}
 }
@@ -41,13 +41,13 @@ func (d *VCenterDriver) FindResourcePool(cluster string, host string, name strin
 	}
 
 	resourcePath := fmt.Sprintf("%v/Resources/%v", res, name)
-	p, err := d.finder.ResourcePool(d.ctx, resourcePath)
+	p, err := d.Finder.ResourcePool(d.Ctx, resourcePath)
 	if err != nil {
 		log.Printf("[WARN] %s not found. Looking for default resource pool.", resourcePath)
-		dp, dperr := d.finder.DefaultResourcePool(d.ctx)
+		dp, dperr := d.Finder.DefaultResourcePool(d.Ctx)
 		var notFoundError *find.NotFoundError
 		if errors.As(dperr, &notFoundError) {
-			vapp, verr := d.finder.VirtualApp(d.ctx, name)
+			vapp, verr := d.Finder.VirtualApp(d.Ctx, name)
 			if verr != nil {
 				return nil, err
 			}
@@ -73,7 +73,7 @@ func (p *ResourcePool) Info(params ...string) (*mo.ResourcePool, error) {
 		params2 = params
 	}
 	var info mo.ResourcePool
-	err := p.pool.Properties(p.driver.ctx, p.pool.Reference(), params2, &info)
+	err := p.pool.Properties(p.driver.Ctx, p.pool.Reference(), params2, &info)
 	if err != nil {
 		return nil, err
 	}
diff --git a/builder/vsphere/driver/vm.go b/builder/vsphere/driver/vm.go
index 36675f1..bfb853c 100644
--- a/builder/vsphere/driver/vm.go
+++ b/builder/vsphere/driver/vm.go
@@ -149,14 +149,14 @@ type CreateConfig struct {
 // NewVM creates a new virtual machine object.
 func (d *VCenterDriver) NewVM(ref *types.ManagedObjectReference) VirtualMachine {
 	return &VirtualMachineDriver{
-		vm:     object.NewVirtualMachine(d.client.Client, *ref),
+		vm:     object.NewVirtualMachine(d.Client.Client, *ref),
 		driver: d,
 	}
 }
 
 // FindVM locates a virtual machine by its name.
 func (d *VCenterDriver) FindVM(name string) (VirtualMachine, error) {
-	vm, err := d.finder.VirtualMachine(d.ctx, name)
+	vm, err := d.Finder.VirtualMachine(d.Ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -282,11 +282,11 @@ func (d *VCenterDriver) CreateVM(config *CreateConfig) (VirtualMachine, error) {
 		VmPathName: fmt.Sprintf("[%s]", datastore.Name()),
 	}
 
-	task, err := folder.folder.CreateVM(d.ctx, createSpec, resourcePool.pool, host)
+	task, err := folder.folder.CreateVM(d.Ctx, createSpec, resourcePool.pool, host)
 	if err != nil {
 		return nil, err
 	}
-	taskInfo, err := task.WaitForResult(d.ctx, nil)
+	taskInfo, err := task.WaitForResult(d.Ctx, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -310,7 +310,7 @@ func (vm *VirtualMachineDriver) Info(params ...string) (*mo.VirtualMachine, erro
 		p = params
 	}
 	var info mo.VirtualMachine
-	err := vm.vm.Properties(vm.driver.ctx, vm.vm.Reference(), p, &info)
+	err := vm.vm.Properties(vm.driver.Ctx, vm.vm.Reference(), p, &info)
 	if err != nil {
 		return nil, err
 	}
@@ -406,7 +406,7 @@ func (vm *VirtualMachineDriver) Clone(ctx context.Context, config *CloneConfig)
 		configSpec.Annotation = config.Annotation
 	}
 
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -465,7 +465,6 @@ func (vm *VirtualMachineDriver) Clone(ctx context.Context, config *CloneConfig)
 			Device:    adapter.(types.BaseVirtualDevice),
 			Operation: types.VirtualDeviceConfigSpecOperationEdit,
 		}
-
 		configSpec.DeviceChange = append(configSpec.DeviceChange, config)
 	}
 
@@ -475,7 +474,7 @@ func (vm *VirtualMachineDriver) Clone(ctx context.Context, config *CloneConfig)
 	}
 	configSpec.VAppConfig = vAppConfig
 
-	task, err := vm.vm.Clone(vm.driver.ctx, folder.folder, config.Name, cloneSpec)
+	task, err := vm.vm.Clone(vm.driver.Ctx, folder.folder, config.Name, cloneSpec)
 	if err != nil {
 		return nil, fmt.Errorf("error calling vm.vm.Clone task: %s", err)
 	}
@@ -559,12 +558,12 @@ func (vm *VirtualMachineDriver) AddPublicKeys(ctx context.Context, publicKeys st
 	}
 
 	confSpec := types.VirtualMachineConfigSpec{VAppConfig: config}
-	task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec)
+	task, err := vm.vm.Reconfigure(vm.driver.Ctx, confSpec)
 	if err != nil {
 		return err
 	}
 
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
@@ -580,11 +579,11 @@ func (vm *VirtualMachineDriver) Properties(ctx context.Context) (*mo.VirtualMach
 
 // Destroy removes the virtual machine.
 func (vm *VirtualMachineDriver) Destroy() error {
-	task, err := vm.vm.Destroy(vm.driver.ctx)
+	task, err := vm.vm.Destroy(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
@@ -628,7 +627,7 @@ func (vm *VirtualMachineDriver) Configure(config *HardwareConfig) error {
 	}
 
 	if config.VideoRAM != 0 || config.Displays != 0 {
-		devices, err := vm.vm.Device(vm.driver.ctx)
+		devices, err := vm.vm.Device(vm.driver.Ctx)
 		if err != nil {
 			return err
 		}
@@ -654,7 +653,7 @@ func (vm *VirtualMachineDriver) Configure(config *HardwareConfig) error {
 	}
 
 	if config.VGPUProfile != "" {
-		devices, err := vm.vm.Device(vm.driver.ctx)
+		devices, err := vm.vm.Device(vm.driver.Ctx)
 		if err != nil {
 			return err
 		}
@@ -709,12 +708,12 @@ func (vm *VirtualMachineDriver) Configure(config *HardwareConfig) error {
 		}
 	}
 
-	task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec)
+	task, err := vm.vm.Reconfigure(vm.driver.Ctx, confSpec)
 	if err != nil {
 		return err
 	}
 
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	if err != nil {
 		return err
 	}
@@ -757,22 +756,22 @@ func (vm *VirtualMachineDriver) Configure(config *HardwareConfig) error {
 // Reconfigure modifies the configuration of an existing virtual machine based
 // on the provided configuration specification.
 func (vm *VirtualMachineDriver) Reconfigure(confSpec types.VirtualMachineConfigSpec) error {
-	task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec)
+	task, err := vm.vm.Reconfigure(vm.driver.Ctx, confSpec)
 	if err != nil {
 		return err
 	}
 
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
 // Customize applies the given CustomizationSpec to the virtual machine.
 func (vm *VirtualMachineDriver) Customize(spec types.CustomizationSpec) error {
-	task, err := vm.vm.Customize(vm.driver.ctx, spec)
+	task, err := vm.vm.Customize(vm.driver.Ctx, spec)
 	if err != nil {
 		return err
 	}
-	return task.Wait(vm.driver.ctx)
+	return task.Wait(vm.driver.Ctx)
 }
 
 // ResizeDisk adjusts the size of the virtual disk to the specified diskSize in
@@ -780,7 +779,7 @@ func (vm *VirtualMachineDriver) Customize(spec types.CustomizationSpec) error {
 // an error if the operation fails.
 // TODO: This method should be refactored to support resizing multiple disks.
 func (vm *VirtualMachineDriver) ResizeDisk(diskSize int64) ([]types.BaseVirtualDeviceConfigSpec, error) {
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -803,11 +802,11 @@ func (vm *VirtualMachineDriver) ResizeDisk(diskSize int64) ([]types.BaseVirtualD
 
 // PowerOn starts the virtual machine and waits for the operation to complete.
 func (vm *VirtualMachineDriver) PowerOn() error {
-	task, err := vm.vm.PowerOn(vm.driver.ctx)
+	task, err := vm.vm.PowerOn(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
@@ -839,7 +838,7 @@ func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet)
 
 // PowerOff stops the virtual machine and waits for the operation to complete.
 func (vm *VirtualMachineDriver) PowerOff() error {
-	state, err := vm.vm.PowerState(vm.driver.ctx)
+	state, err := vm.vm.PowerState(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -848,17 +847,17 @@ func (vm *VirtualMachineDriver) PowerOff() error {
 		return nil
 	}
 
-	task, err := vm.vm.PowerOff(vm.driver.ctx)
+	task, err := vm.vm.PowerOff(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
 // IsPoweredOff checks if the virtual machine is powered off.
 func (vm *VirtualMachineDriver) IsPoweredOff() (bool, error) {
-	state, err := vm.vm.PowerState(vm.driver.ctx)
+	state, err := vm.vm.PowerState(vm.driver.Ctx)
 	if err != nil {
 		return false, err
 	}
@@ -868,7 +867,7 @@ func (vm *VirtualMachineDriver) IsPoweredOff() (bool, error) {
 
 // StartShutdown initiates a guest shutdown operation.
 func (vm *VirtualMachineDriver) StartShutdown() error {
-	err := vm.vm.ShutdownGuest(vm.driver.ctx)
+	err := vm.vm.ShutdownGuest(vm.driver.Ctx)
 	return err
 }
 
@@ -899,22 +898,22 @@ func (vm *VirtualMachineDriver) WaitForShutdown(ctx context.Context, timeout tim
 
 // CreateSnapshot creates a snapshot of the virtual machine.
 func (vm *VirtualMachineDriver) CreateSnapshot(name string) error {
-	task, err := vm.vm.CreateSnapshot(vm.driver.ctx, name, "", false, false)
+	task, err := vm.vm.CreateSnapshot(vm.driver.Ctx, name, "", false, false)
 	if err != nil {
 		return err
 	}
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
 // ConvertToTemplate converts the virtual machine to a template.
 func (vm *VirtualMachineDriver) ConvertToTemplate() error {
-	return vm.vm.MarkAsTemplate(vm.driver.ctx)
+	return vm.vm.MarkAsTemplate(vm.driver.Ctx)
 }
 
 // IsTemplate checks if the virtual machine is a template.
 func (vm *VirtualMachineDriver) IsTemplate() (bool, error) {
-	state, err := vm.vm.IsTemplate(vm.driver.ctx)
+	state, err := vm.vm.IsTemplate(vm.driver.Ctx)
 	if err != nil {
 		return false, err
 	}
@@ -938,12 +937,12 @@ func (vm *VirtualMachineDriver) ConvertToVirtualMachine(vsphereCluster string, v
 		return err
 	}
 
-	return vm.vm.MarkAsVirtualMachine(vm.driver.ctx, *resourcePool.pool, host)
+	return vm.vm.MarkAsVirtualMachine(vm.driver.Ctx, *resourcePool.pool, host)
 }
 
 // ImportOvfToContentLibrary imports the OVF to the content library.
 func (vm *VirtualMachineDriver) ImportOvfToContentLibrary(ovf vcenter.OVF) error {
-	err := vm.driver.restClient.Login(vm.driver.ctx)
+	err := vm.driver.RestClient.Login(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -977,18 +976,18 @@ func (vm *VirtualMachineDriver) ImportOvfToContentLibrary(ovf vcenter.OVF) error
 	ovf.Source.Value = vm.vm.Reference().Value
 	ovf.Source.Type = "VirtualMachine"
 
-	vcm := vcenter.NewManager(vm.driver.restClient.client)
-	_, err = vcm.CreateOVF(vm.driver.ctx, ovf)
+	vcm := vcenter.NewManager(vm.driver.RestClient.client)
+	_, err = vcm.CreateOVF(vm.driver.Ctx, ovf)
 	if err != nil {
 		return err
 	}
 
-	return vm.driver.restClient.Logout(vm.driver.ctx)
+	return vm.driver.RestClient.Logout(vm.driver.Ctx)
 }
 
 // ImportToContentLibrary imports the virtual machine to the content library.
 func (vm *VirtualMachineDriver) ImportToContentLibrary(template vcenter.Template) error {
-	err := vm.driver.restClient.Login(vm.driver.ctx)
+	err := vm.driver.RestClient.Login(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -1052,15 +1051,15 @@ func (vm *VirtualMachineDriver) ImportToContentLibrary(template vcenter.Template
 		template.Placement.Host = h.host.Reference().Value
 	}
 
-	vcm := vcenter.NewManager(vm.driver.restClient.client)
-	_, err = vcm.CreateTemplate(vm.driver.ctx, template)
+	vcm := vcenter.NewManager(vm.driver.RestClient.client)
+	_, err = vcm.CreateTemplate(vm.driver.Ctx, template)
 	if err != nil {
 		log.Printf("cannot create template: %v", err)
 		vm.logout()
 		return err
 	}
 
-	return vm.driver.restClient.Logout(vm.driver.ctx)
+	return vm.driver.RestClient.Logout(vm.driver.Ctx)
 }
 
 // GetDir returns the directory of the virtual machine. Returns an error if the
@@ -1089,7 +1088,7 @@ func addNetwork(d *VCenterDriver, devices object.VirtualDeviceList, config *Crea
 			return nil, err
 		}
 
-		backing, err := network.EthernetCardBackingInfo(d.ctx)
+		backing, err := network.EthernetCardBackingInfo(d.Ctx)
 		if err != nil {
 			return nil, err
 		}
@@ -1161,7 +1160,7 @@ func findNetwork(network string, host string, d *VCenterDriver) (object.NetworkR
 			return nil, fmt.Errorf("more than one network found on host %s, specify the network", host)
 		}
 
-		return object.NewNetwork(d.client.Client, i.Network[0]), nil
+		return object.NewNetwork(d.Client.Client, i.Network[0]), nil
 	}
 
 	return nil, fmt.Errorf("error finding network; 'host' and 'network' not specified. at least one of the two must be specified")
@@ -1233,7 +1232,7 @@ func newVGPUProfile(vGPUProfile string) types.VirtualPCIPassthrough {
 // MountCdrom mounts a CD-ROM to the virtual machine.
 func (vm *VirtualMachineDriver) MountCdrom(controllerType string, datastoreIsoPath string, _cdrom types.BaseVirtualDevice) error {
 	cdrom := _cdrom.(*types.VirtualCdrom)
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -1259,7 +1258,7 @@ func (vm *VirtualMachineDriver) MountCdrom(controllerType string, datastoreIsoPa
 
 // AddCdrom adds a CD-ROM to the virtual machine.
 func (vm *VirtualMachineDriver) AddCdrom(controllerType string, datastoreIsoPath string) error {
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -1300,7 +1299,7 @@ func (vm *VirtualMachineDriver) AddCdrom(controllerType string, datastoreIsoPath
 
 // AddFloppy adds a floppy disk to the virtual machine.
 func (vm *VirtualMachineDriver) AddFloppy(imgPath string) error {
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -1319,7 +1318,7 @@ func (vm *VirtualMachineDriver) AddFloppy(imgPath string) error {
 
 // SetBootOrder sets the boot order of the virtual machine.
 func (vm *VirtualMachineDriver) SetBootOrder(order []string) error {
-	devices, err := vm.vm.Device(vm.driver.ctx)
+	devices, err := vm.vm.Device(vm.driver.Ctx)
 	if err != nil {
 		return err
 	}
@@ -1328,12 +1327,12 @@ func (vm *VirtualMachineDriver) SetBootOrder(order []string) error {
 		BootOrder: devices.BootOrder(order),
 	}
 
-	return vm.vm.SetBootOptions(vm.driver.ctx, &bootOptions)
+	return vm.vm.SetBootOptions(vm.driver.Ctx, &bootOptions)
 }
 
 // RemoveDevice removes a device from the virtual machine.
 func (vm *VirtualMachineDriver) RemoveDevice(keepFiles bool, device ...types.BaseVirtualDevice) error {
-	return vm.vm.RemoveDevice(vm.driver.ctx, keepFiles, device...)
+	return vm.vm.RemoveDevice(vm.driver.Ctx, keepFiles, device...)
 }
 
 // addDevice adds a device to the virtual machine.
@@ -1346,12 +1345,12 @@ func (vm *VirtualMachineDriver) addDevice(device types.BaseVirtualDevice) error
 		return err
 	}
 
-	task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec)
+	task, err := vm.vm.Reconfigure(vm.driver.Ctx, confSpec)
 	if err != nil {
 		return err
 	}
 
-	_, err = task.WaitForResult(vm.driver.ctx, nil)
+	_, err = task.WaitForResult(vm.driver.Ctx, nil)
 	return err
 }
 
@@ -1372,12 +1371,12 @@ func (vm *VirtualMachineDriver) AddConfigParams(params map[string]string, info *
 	confSpec.Tools = info
 
 	if len(confSpec.ExtraConfig) > 0 || confSpec.Tools != nil {
-		task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec)
+		task, err := vm.vm.Reconfigure(vm.driver.Ctx, confSpec)
 		if err != nil {
 			return fmt.Errorf("failed to start reconfiguration task: %w", err)
 		}
 
-		_, err = task.WaitForResult(vm.driver.ctx, nil)
+		_, err = task.WaitForResult(vm.driver.Ctx, nil)
 		if err != nil {
 			return fmt.Errorf("reconfiguration task failed: %w", err)
 		}
@@ -1386,7 +1385,7 @@ func (vm *VirtualMachineDriver) AddConfigParams(params map[string]string, info *
 
 		// Retrieve the current configuration.
 		var moVM mo.VirtualMachine
-		err = vm.vm.Properties(vm.driver.ctx, vm.vm.Reference(), []string{"config.extraConfig"}, &moVM)
+		err = vm.vm.Properties(vm.driver.Ctx, vm.vm.Reference(), []string{"config.extraConfig"}, &moVM)
 		if err != nil {
 			return fmt.Errorf("failed to retrieve current configuration: %w", err)
 		}
@@ -1436,12 +1435,12 @@ func (vm *VirtualMachineDriver) AddFlag(ctx context.Context, flagSpec *types.Vir
 
 // Export exports the virtual machine.
 func (vm *VirtualMachineDriver) Export() (*nfc.Lease, error) {
-	return vm.vm.Export(vm.driver.ctx)
+	return vm.vm.Export(vm.driver.Ctx)
 }
 
 // CreateDescriptor creates a descriptor for the virtual machine used when exporting the virtual machine to an OVF.
 func (vm *VirtualMachineDriver) CreateDescriptor(m *ovf.Manager, cdp types.OvfCreateDescriptorParams) (*types.OvfCreateDescriptorResult, error) {
-	return m.CreateDescriptor(vm.driver.ctx, vm.vm, cdp)
+	return m.CreateDescriptor(vm.driver.Ctx, vm.vm, cdp)
 }
 
 // NewOvfManager creates a new OVF manager instance.
@@ -1452,7 +1451,7 @@ func (vm *VirtualMachineDriver) NewOvfManager() *ovf.Manager {
 // GetOvfExportOptions retrieves the OVF export options for the virtual machine.
 func (vm *VirtualMachineDriver) GetOvfExportOptions(m *ovf.Manager) ([]types.OvfOptionInfo, error) {
 	var mgr mo.OvfManager
-	err := property.DefaultCollector(vm.vm.Client()).RetrieveOne(vm.driver.ctx, m.Reference(), nil, &mgr)
+	err := property.DefaultCollector(vm.vm.Client()).RetrieveOne(vm.driver.Ctx, m.Reference(), nil, &mgr)
 	if err != nil {
 		return nil, err
 	}
@@ -1481,12 +1480,12 @@ func (vm *VirtualMachineDriver) NewNetwork(ref *types.ManagedObjectReference) *N
 
 // Datacenter returns the datacenter of the virtual machine.
 func (vm *VirtualMachineDriver) Datacenter() *object.Datacenter {
-	return vm.driver.datacenter
+	return vm.driver.Datacenter
 }
 
 // FindContentLibraryItemUUID finds a content library item by name.
 func (vm *VirtualMachineDriver) FindContentLibraryItemUUID(library string, name string) (string, error) {
-	err := vm.driver.restClient.Login(vm.driver.ctx)
+	err := vm.driver.RestClient.Login(vm.driver.Ctx)
 	if err != nil {
 		return "", err
 	}
@@ -1510,7 +1509,7 @@ func (vm *VirtualMachineDriver) FindContentLibraryItemUUID(library string, name
 
 // FindContentLibraryTemplateDatastoreName finds the datastore name of the content library template.
 func (vm *VirtualMachineDriver) FindContentLibraryTemplateDatastoreName(library string) ([]string, error) {
-	err := vm.driver.restClient.Login(vm.driver.ctx)
+	err := vm.driver.RestClient.Login(vm.driver.Ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -1530,15 +1529,15 @@ func (vm *VirtualMachineDriver) FindContentLibraryTemplateDatastoreName(library
 		}
 		datastores = append(datastores, name)
 	}
-	return datastores, vm.driver.restClient.Logout(vm.driver.ctx)
+	return datastores, vm.driver.RestClient.Logout(vm.driver.Ctx)
 }
 
 // logout logs the user out of the vCenter.
 func (vm *VirtualMachineDriver) logout() {
-	if vm.driver.restClient == nil {
+	if vm.driver.RestClient == nil {
 		return
 	}
-	if err := vm.driver.restClient.Logout(vm.driver.ctx); err != nil {
+	if err := vm.driver.RestClient.Logout(vm.driver.Ctx); err != nil {
 		log.Printf("cannot logout: %s ", err)
 	}
 }
diff --git a/builder/vsphere/driver/vm_cdrom.go b/builder/vsphere/driver/vm_cdrom.go
index d034fd6..1aff781 100644
--- a/builder/vsphere/driver/vm_cdrom.go
+++ b/builder/vsphere/driver/vm_cdrom.go
@@ -119,7 +119,7 @@ func (vm *VirtualMachineDriver) EjectCdroms() error {
 		c := cd.(*types.VirtualCdrom)
 		c.Backing = &types.VirtualCdromRemotePassthroughBackingInfo{}
 		c.Connectable = &types.VirtualDeviceConnectInfo{}
-		err := vm.vm.EditDevice(vm.driver.ctx, c)
+		err := vm.vm.EditDevice(vm.driver.Ctx, c)
 		if err != nil {
 			return err
 		}
diff --git a/builder/vsphere/driver/vm_keyboard.go b/builder/vsphere/driver/vm_keyboard.go
index 2b8a9b2..2b264b6 100644
--- a/builder/vsphere/driver/vm_keyboard.go
+++ b/builder/vsphere/driver/vm_keyboard.go
@@ -36,7 +36,7 @@ func (vm *VirtualMachineDriver) TypeOnKeyboard(input KeyInput) (int32, error) {
 		Spec: spec,
 	}
 
-	resp, err := methods.PutUsbScanCodes(vm.driver.ctx, vm.driver.client.RoundTripper, req)
+	resp, err := methods.PutUsbScanCodes(vm.driver.Ctx, vm.driver.Client.RoundTripper, req)
 	if err != nil {
 		return 0, err
 	}
diff --git a/datasource/virtualmachine/data.go b/datasource/virtualmachine/data.go
new file mode 100644
index 0000000..8e67e12
--- /dev/null
+++ b/datasource/virtualmachine/data.go
@@ -0,0 +1,171 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+//go:generate packer-sdc struct-markdown
+//go:generate packer-sdc mapstructure-to-hcl2 -type Config,Tag,DatasourceOutput
+
+package virtualmachine
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/hashicorp/hcl/v2/hcldec"
+	"github.com/hashicorp/packer-plugin-sdk/common"
+	"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
+	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
+	"github.com/hashicorp/packer-plugin-sdk/template/config"
+	vsphere "github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/common"
+	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/driver"
+	"github.com/zclconf/go-cty/cty"
+)
+
+type Tag struct {
+	// Name of the tag added to virtual machine which must pass the `tag`
+	// filter.
+	Name string `mapstructure:"name" required:"true"`
+	// Name of the tag category that contains the tag.
+	//
+	// -> **Note:** Both `name` and `category` must be specified in the `tag`
+	// filter.
+	Category string `mapstructure:"category" required:"true"`
+}
+
+type Config struct {
+	common.PackerConfig   `mapstructure:",squash"`
+	vsphere.ConnectConfig `mapstructure:",squash"`
+
+	// Basic filter with glob support (e.g. `ubuntu_basic*`). Defaults to `*`.
+	// Using strict globs will not reduce execution time because vSphere API
+	// returns the full inventory. But can be used for better readability over
+	// regular expressions.
+	Name string `mapstructure:"name"`
+	// Extended name filter with regular expressions support
+	// (e.g. `ubuntu[-_]basic[0-9]*`). Default is empty. The match of the
+	// regular expression is checked by substring. Use `^` and `$` to define a
+	// full string. For example, the `^[^_]+$` filter will search names
+	// without any underscores. The expression must use
+	// [Go Regex Syntax](https://pkg.go.dev/regexp/syntax).
+	NameRegex string `mapstructure:"name_regex"`
+	// Filter to return only objects that are virtual machine templates.
+	// Defaults to `false` and returns all virtual machines.
+	Template bool `mapstructure:"template"`
+	// Filter to search virtual machines only on the specified ESX host.
+	Host string `mapstructure:"host"`
+	// Filter to return only the virtual machines that have attached all specified tags.
+	// Specify one or more `tag` blocks to define list of tags for the filter.
+	//
+	// HCL Example:
+	//
+	// ```hcl
+	//	tag {
+	//	  category = "team"
+	//	  name     = "operations"
+	//	}
+	//	tag {
+	//	  category = "sla"
+	//	  name     = "gold"
+	//	}
+	// ```
+	Tags []Tag `mapstructure:"tag"`
+	// This filter determines how to handle multiple virtual machines that were matched
+	// with all previous filters. Virtual machine creation time is being used to find
+	// the latest. By default, multiple matching machines results in an error.
+	Latest bool `mapstructure:"latest"`
+}
+
+type Datasource struct {
+	config Config
+}
+
+type DatasourceOutput struct {
+	// Name of the found virtual machine.
+	VmName string `mapstructure:"vm_name"`
+}
+
+func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
+	return d.config.FlatMapstructure().HCL2Spec()
+}
+
+func (d *Datasource) Configure(raws ...interface{}) error {
+	err := config.Decode(&d.config, nil, raws...)
+	if err != nil {
+		return err
+	}
+
+	if d.config.Name == "" {
+		d.config.Name = "*"
+	}
+
+	var errs error
+	if d.config.VCenterServer == "" {
+		errs = packersdk.MultiErrorAppend(errs, errors.New("'vcenter_server' is required"))
+	}
+	if d.config.Username == "" {
+		errs = packersdk.MultiErrorAppend(errs, errors.New("'username' is required"))
+	}
+	if d.config.Password == "" {
+		errs = packersdk.MultiErrorAppend(errs, errors.New("'password' is required"))
+	}
+	if len(d.config.Tags) > 0 {
+		for _, tag := range d.config.Tags {
+			if tag.Name == "" || tag.Category == "" {
+				errs = packersdk.MultiErrorAppend(errs, errors.New("both name and category are required for tag"))
+			}
+		}
+	}
+
+	return errs
+}
+
+func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
+	return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
+}
+
+func (d *Datasource) Execute() (cty.Value, error) {
+	driverConfig := &driver.ConnectConfig{
+		VCenterServer:      d.config.VCenterServer,
+		Username:           d.config.Username,
+		Password:           d.config.Password,
+		InsecureConnection: d.config.InsecureConnection,
+		Datacenter:         d.config.Datacenter,
+	}
+
+	dr, err := driver.NewDriver(driverConfig)
+	if err != nil {
+		return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed to initialize driver: %w", err)
+	}
+
+	vcDriver := dr.(*driver.VCenterDriver)
+
+	vmList, err := vcDriver.Finder.VirtualMachineList(vcDriver.Ctx, d.config.Name)
+	if err != nil {
+		return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed to retrieve virtual machines list: %w", err)
+	}
+
+	filteredVms, err := filterVms(vmList, d.config, vcDriver)
+	if err != nil {
+		return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed to filter virtual machines: %w", err)
+	}
+
+	if len(filteredVms) == 0 {
+		return cty.NullVal(cty.EmptyObject), errors.New("no virtual machine matches the filters")
+	}
+
+	if len(filteredVms) > 1 {
+		if d.config.Latest {
+			filteredVms, err = findLatestVM(vcDriver, filteredVms)
+			if err != nil {
+				return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed to find the latest virtual machine: %w", err)
+			}
+		} else {
+			return cty.NullVal(cty.EmptyObject), errors.New("more than one virtual machine matched the filters")
+		}
+	}
+
+	output := DatasourceOutput{
+		VmName: filteredVms[0].Name(),
+	}
+
+	return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
+}
diff --git a/datasource/virtualmachine/data.hcl2spec.go b/datasource/virtualmachine/data.hcl2spec.go
new file mode 100644
index 0000000..31aa2da
--- /dev/null
+++ b/datasource/virtualmachine/data.hcl2spec.go
@@ -0,0 +1,115 @@
+// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
+
+package virtualmachine
+
+import (
+	"github.com/hashicorp/hcl/v2/hcldec"
+	"github.com/zclconf/go-cty/cty"
+)
+
+// FlatConfig is an auto-generated flat version of Config.
+// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
+type FlatConfig struct {
+	PackerBuildName     *string           `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
+	PackerBuilderType   *string           `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
+	PackerCoreVersion   *string           `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
+	PackerDebug         *bool             `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
+	PackerForce         *bool             `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
+	PackerOnError       *string           `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
+	PackerUserVars      map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
+	PackerSensitiveVars []string          `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
+	VCenterServer       *string           `mapstructure:"vcenter_server" cty:"vcenter_server" hcl:"vcenter_server"`
+	Username            *string           `mapstructure:"username" cty:"username" hcl:"username"`
+	Password            *string           `mapstructure:"password" cty:"password" hcl:"password"`
+	InsecureConnection  *bool             `mapstructure:"insecure_connection" cty:"insecure_connection" hcl:"insecure_connection"`
+	Datacenter          *string           `mapstructure:"datacenter" cty:"datacenter" hcl:"datacenter"`
+	Name                *string           `mapstructure:"name" cty:"name" hcl:"name"`
+	NameRegex           *string           `mapstructure:"name_regex" cty:"name_regex" hcl:"name_regex"`
+	Template            *bool             `mapstructure:"template" cty:"template" hcl:"template"`
+	Host                *string           `mapstructure:"host" cty:"host" hcl:"host"`
+	Tags                []FlatTag         `mapstructure:"tag" cty:"tag" hcl:"tag"`
+	Latest              *bool             `mapstructure:"latest" cty:"latest" hcl:"latest"`
+}
+
+// FlatMapstructure returns a new FlatConfig.
+// FlatConfig is an auto-generated flat version of Config.
+// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
+func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
+	return new(FlatConfig)
+}
+
+// HCL2Spec returns the hcl spec of a Config.
+// This spec is used by HCL to read the fields of Config.
+// The decoded values from this spec will then be applied to a FlatConfig.
+func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
+	s := map[string]hcldec.Spec{
+		"packer_build_name":          &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
+		"packer_builder_type":        &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
+		"packer_core_version":        &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false},
+		"packer_debug":               &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
+		"packer_force":               &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
+		"packer_on_error":            &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
+		"packer_user_variables":      &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
+		"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
+		"vcenter_server":             &hcldec.AttrSpec{Name: "vcenter_server", Type: cty.String, Required: false},
+		"username":                   &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
+		"password":                   &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
+		"insecure_connection":        &hcldec.AttrSpec{Name: "insecure_connection", Type: cty.Bool, Required: false},
+		"datacenter":                 &hcldec.AttrSpec{Name: "datacenter", Type: cty.String, Required: false},
+		"name":                       &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
+		"name_regex":                 &hcldec.AttrSpec{Name: "name_regex", Type: cty.String, Required: false},
+		"template":                   &hcldec.AttrSpec{Name: "template", Type: cty.Bool, Required: false},
+		"host":                       &hcldec.AttrSpec{Name: "host", Type: cty.String, Required: false},
+		"tag":                        &hcldec.BlockListSpec{TypeName: "tag", Nested: hcldec.ObjectSpec((*FlatTag)(nil).HCL2Spec())},
+		"latest":                     &hcldec.AttrSpec{Name: "latest", Type: cty.Bool, Required: false},
+	}
+	return s
+}
+
+// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput.
+// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
+type FlatDatasourceOutput struct {
+	VmName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
+}
+
+// FlatMapstructure returns a new FlatDatasourceOutput.
+// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput.
+// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
+func (*DatasourceOutput) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
+	return new(FlatDatasourceOutput)
+}
+
+// HCL2Spec returns the hcl spec of a DatasourceOutput.
+// This spec is used by HCL to read the fields of DatasourceOutput.
+// The decoded values from this spec will then be applied to a FlatDatasourceOutput.
+func (*FlatDatasourceOutput) HCL2Spec() map[string]hcldec.Spec {
+	s := map[string]hcldec.Spec{
+		"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
+	}
+	return s
+}
+
+// FlatTag is an auto-generated flat version of Tag.
+// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
+type FlatTag struct {
+	Name     *string `mapstructure:"name" required:"true" cty:"name" hcl:"name"`
+	Category *string `mapstructure:"category" required:"true" cty:"category" hcl:"category"`
+}
+
+// FlatMapstructure returns a new FlatTag.
+// FlatTag is an auto-generated flat version of Tag.
+// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
+func (*Tag) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
+	return new(FlatTag)
+}
+
+// HCL2Spec returns the hcl spec of a Tag.
+// This spec is used by HCL to read the fields of Tag.
+// The decoded values from this spec will then be applied to a FlatTag.
+func (*FlatTag) HCL2Spec() map[string]hcldec.Spec {
+	s := map[string]hcldec.Spec{
+		"name":     &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
+		"category": &hcldec.AttrSpec{Name: "category", Type: cty.String, Required: false},
+	}
+	return s
+}
diff --git a/datasource/virtualmachine/filters.go b/datasource/virtualmachine/filters.go
new file mode 100644
index 0000000..e588473
--- /dev/null
+++ b/datasource/virtualmachine/filters.go
@@ -0,0 +1,165 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package virtualmachine
+
+import (
+	"fmt"
+	"regexp"
+	"time"
+
+	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/driver"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/property"
+	"github.com/vmware/govmomi/vapi/tags"
+	"github.com/vmware/govmomi/vim25/mo"
+)
+
+// filterVms removes virtual machines from vmList that do not match the datasource config filters.
+func filterVms(vmList []*object.VirtualMachine, c Config, d *driver.VCenterDriver) ([]*object.VirtualMachine, error) {
+	filterFuncs := make([]func(*object.VirtualMachine) (bool, error), 0)
+
+	if c.NameRegex != "" {
+		re := regexp.MustCompile(c.NameRegex)
+		filterFuncs = append(filterFuncs, func(vm *object.VirtualMachine) (bool, error) {
+			return re.MatchString(vm.Name()), nil
+		})
+	}
+
+	if c.Template {
+		filterFuncs = append(filterFuncs, func(vm *object.VirtualMachine) (bool, error) {
+			isTemplate, err := vm.IsTemplate(d.Ctx)
+			if err != nil {
+				return false, fmt.Errorf("error checking if virtual machine is a template: %w", err)
+			}
+			return isTemplate, nil
+		})
+	}
+
+	if c.Host != "" {
+		hostVms, err := getHostVms(d, c.Host)
+		if err != nil {
+			return nil, err
+		}
+
+		filterFuncs = append(filterFuncs, func(vm *object.VirtualMachine) (bool, error) {
+			vmName := vm.Name()
+			for _, hostVm := range hostVms {
+				if vmName == hostVm.Name {
+					return true, nil
+				}
+			}
+			return false, nil
+		})
+	}
+
+	if c.Tags != nil {
+		filterFuncs = append(filterFuncs, func(vm *object.VirtualMachine) (bool, error) {
+			result, err := configTagsMatchHostTags(d, vm, c.Tags)
+			if err != nil {
+				return false, err
+			}
+			return result, nil
+		})
+	}
+
+	result := make([]*object.VirtualMachine, 0)
+	for _, vm := range vmList {
+		var ok bool
+		var err error
+		if len(filterFuncs) == 0 {
+			ok = true
+		}
+		for _, vmPassedFilter := range filterFuncs {
+			ok, err = vmPassedFilter(vm)
+			if err != nil {
+				return nil, fmt.Errorf("failed to filter vm: %w", err)
+			}
+			if !ok {
+				break
+			}
+		}
+		if ok {
+			result = append(result, vm)
+		}
+	}
+
+	return result, nil
+}
+
+// findLatestVM returns the most recently created virtual machine from vmList.
+func findLatestVM(d *driver.VCenterDriver, vmList []*object.VirtualMachine) ([]*object.VirtualMachine, error) {
+	var latestVM *object.VirtualMachine
+	var latestTimestamp time.Time
+	for _, elementVM := range vmList {
+		var vmConfig mo.VirtualMachine
+		err := elementVM.Properties(d.Ctx, elementVM.Reference(), []string{"config"}, &vmConfig)
+		if err != nil {
+			return nil, fmt.Errorf("error retrieving config properties for the virtual machine: %w", err)
+		}
+		if vmConfig.Config.CreateDate.After(latestTimestamp) {
+			latestVM = elementVM
+			latestTimestamp = *vmConfig.Config.CreateDate
+		}
+	}
+	result := []*object.VirtualMachine{latestVM}
+	return result, nil
+}
+
+// getHostVms retrieves all virtual machines on the specified host.
+func getHostVms(d *driver.VCenterDriver, hostName string) ([]mo.VirtualMachine, error) {
+	pc := property.DefaultCollector(d.Client.Client)
+	obj, err := d.Finder.HostSystem(d.Ctx, hostName)
+	if err != nil {
+		return nil, fmt.Errorf("error finding defined host system: %w", err)
+	}
+
+	var host mo.HostSystem
+	err = pc.RetrieveOne(d.Ctx, obj.Reference(), []string{"vm"}, &host)
+	if err != nil {
+		return nil, fmt.Errorf("error retrieving properties of host system: %w", err)
+	}
+
+	var hostVms []mo.VirtualMachine
+	err = pc.Retrieve(d.Ctx, host.Vm, []string{"name"}, &hostVms)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get properties for the virtual machine: %w", err)
+	}
+	return hostVms, nil
+}
+
+// configTagsMatchHostTags returns true if the virtual machine has all tags specified in tagList.
+func configTagsMatchHostTags(d *driver.VCenterDriver, vm *object.VirtualMachine, tagList []Tag) (bool, error) {
+	err := d.RestClient.Login(d.Ctx)
+	if err != nil {
+		return false, fmt.Errorf("failed to login to REST API: %w", err)
+	}
+
+	tagMan := tags.NewManager(d.RestClient.Client())
+	realTagsList, err := tagMan.GetAttachedTags(d.Ctx, vm.Reference())
+	if err != nil {
+		return false, fmt.Errorf("failed return tags for the virtual machine: %w", err)
+	}
+	matchedTagsCount := 0
+	for _, configTag := range tagList {
+		configTagMatched := false
+		for _, realTag := range realTagsList {
+			if configTag.Name == realTag.Name {
+				category, err := tagMan.GetCategory(d.Ctx, realTag.CategoryID)
+				if err != nil {
+					return false, fmt.Errorf("failed to return tag category for tag: %w", err)
+				}
+				if configTag.Category == category.Name {
+					configTagMatched = true
+					break
+				}
+			}
+		}
+		if configTagMatched {
+			matchedTagsCount++
+		} else {
+			break
+		}
+	}
+	return matchedTagsCount == len(tagList), nil
+}
diff --git a/docs-partials/datasource/virtualmachine/Config-not-required.mdx b/docs-partials/datasource/virtualmachine/Config-not-required.mdx
new file mode 100644
index 0000000..61cc59d
--- /dev/null
+++ b/docs-partials/datasource/virtualmachine/Config-not-required.mdx
@@ -0,0 +1,40 @@
+<!-- Code generated from the comments of the Config struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `name` (string) - Basic filter with glob support (e.g. `ubuntu_basic*`). Defaults to `*`.
+  Using strict globs will not reduce execution time because vSphere API
+  returns the full inventory. But can be used for better readability over
+  regular expressions.
+
+- `name_regex` (string) - Extended name filter with regular expressions support
+  (e.g. `ubuntu[-_]basic[0-9]*`). Default is empty. The match of the
+  regular expression is checked by substring. Use `^` and `$` to define a
+  full string. For example, the `^[^_]+$` filter will search names
+  without any underscores. The expression must use
+  [Go Regex Syntax](https://pkg.go.dev/regexp/syntax).
+
+- `template` (bool) - Filter to return only objects that are virtual machine templates.
+  Defaults to `false` and returns all virtual machines.
+
+- `host` (string) - Filter to search virtual machines only on the specified ESX host.
+
+- `tag` ([]Tag) - Filter to return only the virtual machines that have attached all specified tags.
+  Specify one or more `tag` blocks to define list of tags for the filter.
+  
+  HCL Example:
+  
+  ```hcl
+  	tag {
+  	  category = "team"
+  	  name     = "operations"
+  	}
+  	tag {
+  	  category = "sla"
+  	  name     = "gold"
+  	}
+  ```
+
+- `latest` (bool) - This filter determines how to handle multiple virtual machines that were matched
+  with all previous filters. Virtual machine creation time is being used to find
+  the latest. By default, multiple matching machines results in an error.
+
+<!-- End of code generated from the comments of the Config struct in datasource/virtualmachine/data.go; -->
diff --git a/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx b/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx
new file mode 100644
index 0000000..ff30024
--- /dev/null
+++ b/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx
@@ -0,0 +1,5 @@
+<!-- Code generated from the comments of the DatasourceOutput struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `vm_name` (string) - Name of the found virtual machine.
+
+<!-- End of code generated from the comments of the DatasourceOutput struct in datasource/virtualmachine/data.go; -->
diff --git a/docs-partials/datasource/virtualmachine/Tag-required.mdx b/docs-partials/datasource/virtualmachine/Tag-required.mdx
new file mode 100644
index 0000000..0b9d544
--- /dev/null
+++ b/docs-partials/datasource/virtualmachine/Tag-required.mdx
@@ -0,0 +1,11 @@
+<!-- Code generated from the comments of the Tag struct in datasource/virtualmachine/data.go; DO NOT EDIT MANUALLY -->
+
+- `name` (string) - Name of the tag added to virtual machine which must pass the `tag`
+  filter.
+
+- `category` (string) - Name of the tag category that contains the tag.
+  
+  -> **Note:** Both `name` and `category` must be specified in the `tag`
+  filter.
+
+<!-- End of code generated from the comments of the Tag struct in datasource/virtualmachine/data.go; -->
diff --git a/docs/README.md b/docs/README.md
index 626fb9e..deb9ac7 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -49,6 +49,11 @@ packer plugins install github.com/hashicorp/vsphere
   This builder deploys and publishes new virtual machine to a vSphere Supervisor cluster using VM
   Service.
 
+#### Data Sources
+
+- [vsphere-virtualmachine](/packer/integrations/hashicorp/vsphere/latest/components/data-source/vsphere-virtualmachine) -
+  This data source returns the name of a virtual machine that matches all defined filters.
+
 #### Post-Processors
 
 - [vsphere](/packer/integrations/hashicorp/vsphere/latest/components/post-processor/vsphere) -
diff --git a/docs/datasources/virtualmachine.mdx b/docs/datasources/virtualmachine.mdx
new file mode 100644
index 0000000..db7d9ff
--- /dev/null
+++ b/docs/datasources/virtualmachine.mdx
@@ -0,0 +1,171 @@
+---
+modeline: |
+  vim: set ft=pandoc:
+description: |
+  This data source retrieves information about existing virtual machines from vSphere
+  and returns the name of a virtual machine that matches all specified filters. This
+  virtual machine can be used in the vSphere Clone builder to select a template.
+page_title: vSphere Virtual Machine - Data Source
+sidebar_title: vSphere Virtual Machine
+---
+
+# Virtual Machine Data Source
+
+Type: `vsphere-virtualmachine`
+Artifact BuilderId: `vsphere.virtualmachine`
+
+This data source retrieves information about existing virtual machines from vSphere
+and returns the name of a virtual machine that matches all specified filters. This
+virtual machine can be used in the vSphere Clone builder to select a template.
+
+## Configuration Reference
+
+### Filters Configuration
+
+**Optional:**
+
+@include 'datasource/virtualmachine/Config-not-required.mdx'
+
+### Tags Filter Configuration
+
+**Required:**
+
+@include 'datasource/virtualmachine/Tag-required.mdx'
+
+### Connection Configuration
+
+**Optional:**
+
+@include 'builder/vsphere/common/ConnectConfig-not-required.mdx'
+
+## Output
+
+@include 'datasource/virtualmachine/DatasourceOutput.mdx'
+
+## Example Usage
+
+### Filter by Tags and Get Latest
+
+This example demonstrates how to connect to vSphere cluster and search for the latest
+virtual machine that matches the specified tag filters.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  latest         = true
+
+  tag {
+    category = "team"
+    name     = "operations"
+  }
+
+  tag {
+    category = "sla"
+    name     = "gold"
+  }
+}
+
+locals {
+  vm_name = data.vsphere-virtualmachine.example.vm_name
+}
+
+source "null" "example" {
+    communicator = "none"
+}
+
+build {
+  sources = [
+    "source.null.example"
+  ]
+
+  provisioner "shell-local" {
+    inline = [
+      "echo vm_name: ${local.vm_name}",
+    ]
+  }
+}
+```
+
+### Filter by Name with Glob Pattern
+
+This example shows how to use the `name` filter with glob patterns to find virtual
+machines matching a specific naming convention.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name           = "linux-debian-13-*"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter by Name with Regular Expression
+
+This example demonstrates using the `name_regex` filter for more complex pattern
+matching with regular expressions.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name_regex     = "^linux-debian-[0-9]+-.+$"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter for VM Templates Only
+
+This example shows how to use the `template` filter to search only for virtual
+machine templates.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  name           = "linux-debian-13-*"
+  template       = true
+}
+
+locals {
+  result_template = data.vsphere-virtualmachine.example.vm_name
+}
+```
+
+### Filter by ESX Host
+
+This example demonstrates filtering virtual machines by a specific ESX host,
+combined with other filters.
+
+```hcl
+data "vsphere-virtualmachine" "example" {
+  vcenter_server = "vcenter.example.com"
+  username       = "administrator@vsphere.local"
+  password       = "VMware1!"
+  datacenter     = "dc-01"
+  host           = "esxi-01.example.com"
+  name           = "linux-debian-13-*"
+  latest         = true
+}
+
+locals {
+  vm_result = data.vsphere-virtualmachine.example.vm_name
+}
+```
\ No newline at end of file
diff --git a/main.go b/main.go
index 19eb19e..43a1b96 100644
--- a/main.go
+++ b/main.go
@@ -10,6 +10,7 @@ import (
 	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/clone"
 	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/iso"
 	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/supervisor"
+	"github.com/hashicorp/packer-plugin-vsphere/datasource/virtualmachine"
 	"github.com/hashicorp/packer-plugin-vsphere/post-processor/vsphere"
 	vsphereTemplate "github.com/hashicorp/packer-plugin-vsphere/post-processor/vsphere-template"
 	"github.com/hashicorp/packer-plugin-vsphere/version"
@@ -20,6 +21,7 @@ func main() {
 	pps.RegisterBuilder("iso", new(iso.Builder))
 	pps.RegisterBuilder("clone", new(clone.Builder))
 	pps.RegisterBuilder("supervisor", new(supervisor.Builder))
+	pps.RegisterDatasource("virtualmachine", new(virtualmachine.Datasource))
 	pps.RegisterPostProcessor(plugin.DEFAULT_NAME, new(vsphere.PostProcessor))
 	pps.RegisterPostProcessor("template", new(vsphereTemplate.PostProcessor))
 	pps.SetVersion(version.PluginVersion)



Test patch:
diff --git a/builder/vsphere/driver/driver_test.go b/builder/vsphere/driver/driver_test.go
index 109b6f1..b578d84 100644
--- a/builder/vsphere/driver/driver_test.go
+++ b/builder/vsphere/driver/driver_test.go
@@ -152,15 +152,15 @@ func (s *VCenterSimulator) NewSimulatorDriver() (*VCenterDriver, error) {
 	finder.SetDatacenter(datacenter)
 
 	d := &VCenterDriver{
-		ctx:       ctx,
-		client:    client,
-		vimClient: vimClient,
-		restClient: &RestClient{
+		Ctx:       ctx,
+		Client:    client,
+		VimClient: vimClient,
+		RestClient: &RestClient{
 			client:      rest.NewClient(vimClient),
 			credentials: user,
 		},
-		datacenter: datacenter,
-		finder:     finder,
+		Datacenter: datacenter,
+		Finder:     finder,
 	}
 	return d, nil
 }
diff --git a/builder/vsphere/iso/builder_acc_test.go b/builder/vsphere/iso/builder_acc_test.go
index bd34c19..5ad807e 100644
--- a/builder/vsphere/iso/builder_acc_test.go
+++ b/builder/vsphere/iso/builder_acc_test.go
@@ -10,8 +10,8 @@ import (
 	"testing"
 
 	"github.com/hashicorp/packer-plugin-sdk/acctest"
-	commonT "github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/common/testing"
 	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/common/utils"
+	"github.com/hashicorp/packer-plugin-vsphere/testing/vsphere"
 	"github.com/vmware/govmomi/vim25/types"
 )
 
@@ -20,13 +20,13 @@ func TestAccISOBuilderAcc_default(t *testing.T) {
 	config := defaultConfig()
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_basic_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -57,7 +57,7 @@ func defaultConfig() map[string]interface{} {
 		"ssh_username": "packer",
 		"ssh_password": "VMw@re1!",
 
-		"vm_name": commonT.NewVMName(),
+		"vm_name": vsphere.NewVmName(),
 		"storage": map[string]interface{}{
 			"disk_size": 2048,
 		},
@@ -70,7 +70,7 @@ func defaultConfig() map[string]interface{} {
 
 // checkDefault verifies the configuration of a virtual machine by comparing its properties against expected values.
 func checkDefault(name string, host string, datastore string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -139,13 +139,13 @@ func TestAccISOBuilderAcc_notes(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_notes_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -161,7 +161,7 @@ func TestAccISOBuilderAcc_notes(t *testing.T) {
 
 // checkNotes verifies a virtual machine has the "config.annotation" field set.
 func checkNotes(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -197,13 +197,13 @@ func TestAccISOBuilderAcc_hardware(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_hardware_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -219,7 +219,7 @@ func TestAccISOBuilderAcc_hardware(t *testing.T) {
 
 // checkHardware verifies a virtual machine hardware configuration.
 func checkHardware(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -303,13 +303,13 @@ func TestAccISOBuilderAcc_limit(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_limit_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -325,7 +325,7 @@ func TestAccISOBuilderAcc_limit(t *testing.T) {
 
 // checkLimit verifies a virtual machine unlimited CPU allocation limit.
 func checkLimit(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -353,13 +353,13 @@ func TestAccISOBuilderAcc_sata(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_sata_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -375,7 +375,7 @@ func TestAccISOBuilderAcc_sata(t *testing.T) {
 
 // checkSata verifies a virtual machine SATA controller configuration.
 func checkSata(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -406,13 +406,13 @@ func TestAccISOBuilderAcc_cdrom(t *testing.T) {
 	}
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_cdrom_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -434,13 +434,13 @@ func TestAccISOBuilderAcc_networkCard(t *testing.T) {
 	}
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_networkCard_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -456,7 +456,7 @@ func TestAccISOBuilderAcc_networkCard(t *testing.T) {
 
 // checkNetworkCard verifies a virtual machine network card configuration.
 func checkNetworkCard(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -501,13 +501,13 @@ func TestAccISOBuilderAcc_createFloppy(t *testing.T) {
 	config["floppy_files"] = []string{tmpFile.Name()}
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_createFloppy_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("unexpected error: expected 'nil', but returned '%s'", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -526,13 +526,13 @@ func TestAccISOBuilderAcc_full(t *testing.T) {
 	config := fullConfig()
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_full_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -560,7 +560,7 @@ func fullConfig() map[string]interface{} {
 		"host":                host,
 		"insecure_connection": true,
 
-		"vm_name": commonT.NewVMName(),
+		"vm_name": vsphere.NewVmName(),
 
 		"RAM": 512,
 		"disk_controller_type": []string{
@@ -611,7 +611,7 @@ func fullConfig() map[string]interface{} {
 
 // checkFull verifies a virtual machine full configuration.
 func checkFull(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -651,13 +651,13 @@ func TestAccISOBuilderAcc_bootOrder(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_bootOrder_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -673,7 +673,7 @@ func TestAccISOBuilderAcc_bootOrder(t *testing.T) {
 
 // checkBootOrder verifies a virtual machine boot order configuration.
 func checkBootOrder(name string) error {
-	d, err := commonT.TestConn()
+	d, err := vsphere.TestConn()
 	if err != nil {
 		return fmt.Errorf("cannot connect %v", err)
 	}
@@ -702,13 +702,13 @@ func TestISOBuilderAcc_cluster(t *testing.T) {
 	config["host"] = "esxi-02.example.com"
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_bootOrder_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
@@ -734,13 +734,13 @@ func TestISOBuilderAcc_clusterDRS(t *testing.T) {
 
 	testCase := &acctest.PluginTestCase{
 		Name:     "vsphere-iso_bootOrder_test",
-		Template: commonT.RenderConfig("vsphere-iso", config),
+		Template: vsphere.RenderConfig("vsphere-iso", config),
 		Teardown: func() error {
-			d, err := commonT.TestConn()
+			d, err := vsphere.TestConn()
 			if err != nil {
 				return fmt.Errorf("cannot connect %v", err)
 			}
-			return commonT.CleanupVM(d, config["vm_name"].(string))
+			return vsphere.CleanupVm(d, config["vm_name"].(string))
 		},
 		Check: func(buildCommand *exec.Cmd, logfile string) error {
 			if buildCommand.ProcessState != nil {
diff --git a/datasource/virtualmachine/data_test.go b/datasource/virtualmachine/data_test.go
new file mode 100644
index 0000000..e04519b
--- /dev/null
+++ b/datasource/virtualmachine/data_test.go
@@ -0,0 +1,212 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package virtualmachine
+
+import (
+	"testing"
+	"time"
+
+	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/common"
+	"github.com/hashicorp/packer-plugin-vsphere/testing/vsphere"
+	"github.com/vmware/govmomi/simulator"
+)
+
+func TestDatasource_Execute(t *testing.T) {
+	machinesToPrepare := []vsphere.SimulatedVmConfig{
+		{
+			Name: "first-vm",
+			Tags: []vsphere.Tag{
+				{
+					Category: "operating-system-class",
+					Name:     "Linux",
+				},
+			},
+		}, {
+			Name: "second-vm",
+			Tags: []vsphere.Tag{
+				{
+					Category: "operating-system-class",
+					Name:     "Linux",
+				},
+				{
+					Category: "security-team",
+					Name:     "red",
+				},
+				{
+					Category: "security-team",
+					Name:     "blue",
+				},
+			},
+			Template: true,
+		}, {
+			Name: "machine-three",
+			Tags: []vsphere.Tag{
+				{
+					Category: "operating-system-class",
+					Name:     "Linux",
+				},
+				{
+					Category: "security-team",
+					Name:     "blue",
+				},
+			},
+			CreationTime: time.Now().AddDate(0, 0, 1),
+		},
+	}
+
+	model := simulator.VPX()
+	model.Datacenter = 2
+	model.Machine = 8
+
+	vcSim, err := vsphere.NewSimulator(model)
+	if err != nil {
+		t.Fatalf("error creating vCenter simulator: %s", err)
+	}
+	defer vcSim.Stop()
+
+	err = vcSim.ApplyVmConfiguration(machinesToPrepare)
+	if err != nil {
+		t.Fatalf("error customizing simulator: %s", err)
+	}
+
+	simulatorPassword, _ := vcSim.Server.URL.User.Password()
+	connectConfig := common.ConnectConfig{
+		VCenterServer:      vcSim.Server.URL.Host,
+		Username:           vcSim.Server.URL.User.Username(),
+		Password:           simulatorPassword,
+		InsecureConnection: true,
+		Datacenter:         vcSim.Datacenter.Name(),
+	}
+
+	dsTestConfigs := []struct {
+		name          string
+		expectFailure bool
+		expectVmName  string
+		config        Config
+	}{
+		{
+			name:          "first-vm was found by name, no error",
+			expectFailure: false,
+			expectVmName:  "first-vm",
+			config: Config{
+				Name: "first-vm",
+			},
+		},
+		{
+			name:          "no machines match the filter, error",
+			expectFailure: true,
+			expectVmName:  "",
+			config: Config{
+				Name: "firstest-vm",
+			},
+		},
+		{
+			name:          "second-vm was found by the regex, no error",
+			expectFailure: false,
+			expectVmName:  "second-vm",
+			config: Config{
+				NameRegex: "^seco.*m$",
+			},
+		},
+		{
+			name:          "multiple machines match the regex, but latest not used, error",
+			expectFailure: true,
+			expectVmName:  "",
+			config: Config{
+				NameRegex: ".*-vm",
+			},
+		},
+		{
+			name:          "multiple guests match the regex and latest used, no error",
+			expectFailure: false,
+			expectVmName:  "machine-three",
+			config: Config{
+				NameRegex: "^[^_]+$",
+				Latest:    true,
+			},
+		},
+		{
+			name:          "found machine that is a template, no error",
+			expectFailure: false,
+			expectVmName:  "second-vm",
+			config: Config{
+				Template: true,
+			},
+		},
+		{
+			name:          "found multiple machines at the host, error",
+			expectFailure: true,
+			expectVmName:  "",
+			config: Config{
+				Host: "DC0_H0",
+			},
+		},
+		{
+			name:          "cluster host not found, error",
+			expectFailure: true,
+			expectVmName:  "",
+			config: Config{
+				Host: "unexpected_host",
+			},
+		},
+		{
+			name:          "found machine with defined set of tags, no error",
+			expectFailure: false,
+			expectVmName:  "second-vm",
+			config: Config{
+				Tags: []Tag{
+					{
+						Category: "security-team",
+						Name:     "blue",
+					},
+					{
+						Category: "security-team",
+						Name:     "red",
+					},
+				},
+			},
+		},
+		{
+			name:          "found multiple machines with defined set of tags, error",
+			expectFailure: true,
+			expectVmName:  "",
+			config: Config{
+				Tags: []Tag{
+					{
+						Category: "operating-system-class",
+						Name:     "Linux",
+					},
+				},
+			},
+		},
+	}
+
+	for _, testConfig := range dsTestConfigs {
+		t.Run(testConfig.name, func(t *testing.T) {
+			testConfig.config.ConnectConfig = connectConfig
+
+			ds := Datasource{
+				config: testConfig.config,
+			}
+			err := ds.Configure()
+			if err != nil {
+				t.Fatalf("Failed to configure datasource: %s", err)
+			}
+
+			result, err := ds.Execute()
+			if err != nil && !testConfig.expectFailure {
+				t.Fatalf("unexpected failure: %s", err)
+			}
+			if err == nil && testConfig.expectFailure {
+				t.Errorf("expected failure, but execution succeeded")
+			}
+			if err == nil {
+				vmName := result.GetAttr("vm_name").AsString()
+				if vmName != testConfig.expectVmName {
+					t.Errorf("expected vm name `%s`, but got `%s`", testConfig.expectVmName, vmName)
+				}
+			}
+		})
+	}
+}
diff --git a/builder/vsphere/common/testing/utility.go b/testing/vsphere/acceptance.go
similarity index 88%
rename from builder/vsphere/common/testing/utility.go
rename to testing/vsphere/acceptance.go
index a9bde19..dda908c 100644
--- a/builder/vsphere/common/testing/utility.go
+++ b/testing/vsphere/acceptance.go
@@ -1,7 +1,7 @@
 // Copyright (c) HashiCorp, Inc.
 // SPDX-License-Identifier: MPL-2.0
 
-package testing
+package vsphere
 
 import (
 	"encoding/json"
@@ -13,7 +13,7 @@ import (
 	"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/driver"
 )
 
-func NewVMName() string {
+func NewVmName() string {
 	r := rand.New(rand.NewSource(time.Now().UnixNano()))
 	return fmt.Sprintf("test-%v", r.Intn(1000))
 }
@@ -49,7 +49,7 @@ func TestConn() (driver.Driver, error) {
 	return d, nil
 }
 
-func GetVM(d driver.Driver, name string) (driver.VirtualMachine, error) {
+func getVm(d driver.Driver, name string) (driver.VirtualMachine, error) {
 	vm, err := d.FindVM(name)
 	if err != nil {
 		return nil, fmt.Errorf("error finding virtual machine: %v", err)
@@ -57,8 +57,8 @@ func GetVM(d driver.Driver, name string) (driver.VirtualMachine, error) {
 	return vm, nil
 }
 
-func CleanupVM(d driver.Driver, name string) error {
-	vm, err := GetVM(d, name)
+func CleanupVm(d driver.Driver, name string) error {
+	vm, err := getVm(d, name)
 	if err != nil {
 		return fmt.Errorf("error finding virtual machine: %v", err)
 	}
diff --git a/testing/vsphere/helpers.go b/testing/vsphere/helpers.go
new file mode 100644
index 0000000..b7b8c55
--- /dev/null
+++ b/testing/vsphere/helpers.go
@@ -0,0 +1,65 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package vsphere
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vapi/tags"
+)
+
+// markSimulatedVmAsTemplate powers off the virtual machine and converts it to a template.
+func markSimulatedVmAsTemplate(ctx context.Context, vm *object.VirtualMachine) error {
+	task, err := vm.PowerOff(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to issue powering off command to the machine: %w", err)
+	}
+	err = task.Wait(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to power off the machine: %w", err)
+	}
+	err = vm.MarkAsTemplate(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to mark virtual machine as a template: %w", err)
+	}
+	return nil
+}
+
+// ensureCategory ensures a tag category exists by name and returns its ID.
+func ensureCategory(ctx context.Context, man *tags.Manager, catName string) (string, error) {
+	categoryList, err := man.GetCategories(ctx)
+	if err != nil {
+		return "", fmt.Errorf("cannot return categories from cluster: %w", err)
+	}
+	for _, category := range categoryList {
+		if category.Name == catName {
+			return category.ID, nil
+		}
+	}
+	newCategoryID, err := man.CreateCategory(ctx, &tags.Category{Name: catName, Cardinality: "MULTIPLE"})
+	if err != nil {
+		return "", fmt.Errorf("cannot create category: %w", err)
+	}
+	return newCategoryID, nil
+}
+
+// ensureTag ensures a tag exists within the specified category and returns its ID.
+func ensureTag(ctx context.Context, man *tags.Manager, catID string, tagName string) (string, error) {
+	tagsInCategory, err := man.GetTagsForCategory(ctx, catID)
+	if err != nil {
+		return "", fmt.Errorf("cannot return tags for category: %w", err)
+	}
+	for _, tag := range tagsInCategory {
+		if tag.Name == tagName {
+			return tag.ID, nil
+		}
+	}
+	newTagID, err := man.CreateTag(ctx, &tags.Tag{Name: tagName, CategoryID: catID})
+	if err != nil {
+		return "", fmt.Errorf("cannot create tag: %w", err)
+	}
+	return newTagID, nil
+}
diff --git a/testing/vsphere/simulator.go b/testing/vsphere/simulator.go
new file mode 100644
index 0000000..bd33592
--- /dev/null
+++ b/testing/vsphere/simulator.go
@@ -0,0 +1,163 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package vsphere
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net/url"
+	"time"
+
+	"github.com/vmware/govmomi"
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/rest"
+	_ "github.com/vmware/govmomi/vapi/simulator"
+	"github.com/vmware/govmomi/vapi/tags"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type Tag struct {
+	Category string
+	Name     string
+}
+
+type SimulatedVmConfig struct {
+	Name         string
+	Tags         []Tag
+	Template     bool
+	CreationTime time.Time
+}
+
+type simulatorContext struct {
+	Model      *simulator.Model
+	Server     *simulator.Server
+	Ctx        context.Context
+	Client     *govmomi.Client
+	RestClient *rest.Client
+	Finder     *find.Finder
+	Datacenter *object.Datacenter
+}
+
+// NewSimulator creates a new vCenter simulator with the specified model.
+func NewSimulator(model *simulator.Model) (*simulatorContext, error) {
+	ctx := context.Background()
+	if model == nil {
+		return nil, fmt.Errorf("model has not been initialized")
+	}
+
+	err := model.Create()
+	if err != nil {
+		return nil, fmt.Errorf("failed to create simulator model: %w", err)
+	}
+	model.Service.RegisterEndpoints = true
+	model.Service.TLS = new(tls.Config)
+
+	server := model.Service.NewServer()
+
+	u, err := url.Parse(server.URL.String())
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse simulator URL: %w", err)
+	}
+	password, _ := simulator.DefaultLogin.Password()
+	u.User = url.UserPassword(simulator.DefaultLogin.Username(), password)
+
+	client, err := govmomi.NewClient(ctx, u, true)
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to SOAP simulator: %w", err)
+	}
+
+	restClient := rest.NewClient(client.Client)
+	err = restClient.Login(ctx, simulator.DefaultLogin)
+	if err != nil {
+		return nil, fmt.Errorf("failed to login to REST simulator: %w", err)
+	}
+
+	finder := find.NewFinder(client.Client, false)
+	dcs, err := finder.DatacenterList(ctx, "*")
+	if err != nil {
+		return nil, fmt.Errorf("failed to list datacenters: %w", err)
+	}
+	if len(dcs) == 0 {
+		return nil, fmt.Errorf("datacenters were not found in the simulator: %w", err)
+	}
+	finder.SetDatacenter(dcs[0])
+
+	return &simulatorContext{
+		Ctx:        ctx,
+		Server:     server,
+		Model:      model,
+		Client:     client,
+		Finder:     finder,
+		RestClient: restClient,
+		Datacenter: dcs[0],
+	}, nil
+}
+
+func (sim *simulatorContext) Stop() {
+	if sim.Model != nil {
+		sim.Model.Remove()
+	}
+	if sim.Server != nil {
+		sim.Server.Close()
+	}
+}
+
+// ApplyVmConfiguration applies virtual machines in the simulator according to the provided configurations.
+func (sim *simulatorContext) ApplyVmConfiguration(vmsConfig []SimulatedVmConfig) error {
+	tagMan := tags.NewManager(sim.RestClient)
+
+	vms, err := sim.Finder.VirtualMachineList(sim.Ctx, "*")
+	if err != nil {
+		return fmt.Errorf("failed to list virtual machines in cluster: %w", err)
+	}
+
+	for i := 0; i < len(vmsConfig); i++ {
+		vmConfig := types.VirtualMachineConfigSpec{
+			Name: vmsConfig[i].Name,
+		}
+
+		if !vmsConfig[i].CreationTime.IsZero() {
+			vmConfig.CreateDate = &vmsConfig[i].CreationTime
+		}
+
+		if vmsConfig[i].Name != "" {
+			task, err := vms[i].Reconfigure(sim.Ctx, vmConfig)
+			if err != nil {
+				return fmt.Errorf("failed to issue rename of virtual machine command: %w", err)
+			}
+			if err = task.Wait(sim.Ctx); err != nil {
+				return fmt.Errorf("failed to rename virtual machine: %w", err)
+			}
+		}
+
+		if vmsConfig[i].Template {
+			err = markSimulatedVmAsTemplate(sim.Ctx, vms[i])
+			if err != nil {
+				return fmt.Errorf("failed to convert to templates: %w", err)
+			}
+		}
+
+		if vmsConfig[i].Tags != nil {
+			for _, tag := range vmsConfig[i].Tags {
+				catID, err := ensureCategory(sim.Ctx, tagMan, tag.Category)
+				if err != nil {
+					return fmt.Errorf("failed to ensure category exists: %w", err)
+				}
+				tagID, err := ensureTag(sim.Ctx, tagMan, catID, tag.Name)
+				if err != nil {
+					return fmt.Errorf("failed to ensure tag exists: %w", err)
+				}
+				err = tagMan.AttachTag(sim.Ctx, tagID, vms[i].Reference())
+				if err != nil {
+					return fmt.Errorf("failed to attach tag to virtual machine: %w", err)
+				}
+			}
+		}
+	}
+
+	return nil
+}