Repository: kubermatic/machine-controller
Pull request #1217 (2022-03-21 15:57:47)

Kubevirt Add common label to all VMs linked to the same MachineDeployment
Signed-off-by: Helene Durand <helene@kubermatic.com>

**What this PR does / why we need it**:
This PR adds a common label `machine: md-<machine-id>` to all VM spawned by the same MachineDeployment.
We already have in all `machine` this common label. Re-using it to add it to the `VirtualMachines`.
Also taking this PR opportunity to update the `example` for the Kubevirt MachineDeployment specification.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

**Optional Release Note**:
<!--
Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
NONE
```


kubermatic-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1217#" title="Author self-approved">hdurand0710</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **moadqassem** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @moadqassem` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubermatic%2Fmachine-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubermatic/machine-controller/blob/master/OWNERS)**
- **[pkg/cloudprovider/provider/OWNERS](https://github.com/kubermatic/machine-controller/blob/master/pkg/cloudprovider/provider/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["moadqassem"]} -->
hdurand0710: /test pull-machine-controller-e2e-kubevirt
sankalp-r: /lgtm
kubermatic-bot: LGTM label has been added.  <details>Git tree hash: 8d8bc91cda094cde12f96a36dc812d4541c83e47</details>
moadqassem: /hold
kubermatic-bot: New changes are detected. LGTM label has been removed.
sankalp-r: /lgtm

Patch:
diff --git a/.gitignore b/.gitignore
index b782ea31..b5a1dac6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ examples/*.csr
 examples/*.srl
 /webhook
 /vendor
-.vscode
\ No newline at end of file
+.vscode
+.gitpod.yml
\ No newline at end of file
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 23f2f754..1d4d78a3 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -4,14 +4,15 @@ aliases:
   machine-controller-maintainers:
     - ahmedwaleedmalik
     - embik
+    - hdurand0710
     - kron4eg
     - mfranczy
     - moadqassem
     - moelsayed
+    - sankalp-r
     - themue
     - xmudrii
     - xrstf
-    - youssefazrak
 
   # Temporary SIG to oversee changes in userdata and cloudprovider sub-directories
   # This SIG is responsible for ensuring that OSM and machine-controller are in sync
diff --git a/examples/kubevirt-machinedeployment.yaml b/examples/kubevirt-machinedeployment.yaml
index f8bc5c29..20c423ca 100644
--- a/examples/kubevirt-machinedeployment.yaml
+++ b/examples/kubevirt-machinedeployment.yaml
@@ -26,14 +26,17 @@ spec:
             - "<< YOUR_PUBLIC_KEY >>"
           cloudProvider: "kubevirt"
           cloudProviderSpec:
-            storageClassName: kubermatic-fast
-            pvcSize: "10Gi"
-            sourceURL: http://10.109.79.210/<< OS_NAME >>.img
-            cpus: "1"
-            memory: "2048M"
-            kubeconfig:
-              value: '<< KUBECONFIG >>'
-            namespace: kube-system
+            auth:
+              kubeconfig:
+                value: '<< KUBECONFIG >>'
+            virtualMachine:
+              template:
+                cpus: "1"
+                memory: "2048M"
+                primaryDisk:
+                  osImage: http://10.109.79.210/<< OS_NAME >>.img
+                  size: "10Gi"
+                  storageClassName: kubermatic-fast
           # Can also be `centos`, must align with he configured registryImage above
           operatingSystem: "ubuntu"
           operatingSystemSpec:
diff --git a/go.mod b/go.mod
index abd9200b..7fb3d9fd 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/aliyun/alibaba-cloud-sdk-go v1.61.751
-	github.com/anexia-it/go-anxcloud v0.3.8
+	github.com/anexia-it/go-anxcloud v0.3.26
 	github.com/aws/aws-sdk-go v1.36.2
 	github.com/coreos/container-linux-config-transpiler v0.9.0
 	github.com/davecgh/go-spew v1.1.1
diff --git a/go.sum b/go.sum
index 5cb0cbda..a078d09d 100644
--- a/go.sum
+++ b/go.sum
@@ -173,8 +173,9 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.61.751 h1:PX0jCn9kBBgaybsFltpmQ8F7O74h
 github.com/aliyun/alibaba-cloud-sdk-go v1.61.751/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/andygrunwald/go-gerrit v0.0.0-20190120104749-174420ebee6c/go.mod h1:0iuRQp6WJ44ts+iihy5E/WlPqfg5RNeQxOmzRkxCdtk=
-github.com/anexia-it/go-anxcloud v0.3.8 h1:+ZOVqUHwINTm9Q68GPVh+Q/c794Fe+2GahIVagNLjDg=
 github.com/anexia-it/go-anxcloud v0.3.8/go.mod h1:cevqezsbOJ4GBlAWaztfLKl9w4VzxJBt4ipgHORi3gw=
+github.com/anexia-it/go-anxcloud v0.3.26 h1:uStosj8srS6OA1OsPsMJBFqd4Znzl6fEhUv8b3+G8FU=
+github.com/anexia-it/go-anxcloud v0.3.26/go.mod h1:fiEBxEtBXx78/OWBJvL7+2o4TESrnEcrDYjLeonGkDw=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -320,6 +321,7 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -1004,6 +1006,7 @@ github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
 github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
 github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
 github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
@@ -1182,7 +1185,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
@@ -1606,7 +1608,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1987,7 +1988,6 @@ k8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI=
 k8s.io/api v0.19.4/go.mod h1:SbtJ2aHCItirzdJ36YslycFNzWADYH3tgOhvBEFtZAk=
 k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
 k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8=
-k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8=
 k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw=
 k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8=
 k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY=
@@ -2026,7 +2026,6 @@ k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlm
 k8s.io/apimachinery v0.19.4/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
 k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
-k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk=
 k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
 k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg=
@@ -2112,8 +2111,6 @@ k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/kube-aggregator v0.19.0/go.mod h1:1Ln45PQggFAG8xOqWPIYMxUq8WNtpPnYsbUJ39DpF/A=
 k8s.io/kube-aggregator v0.19.4/go.mod h1:cTkvun110194d797AuThyydBBlgm+cKIFUeS2uzGJfU=
 k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
-k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
-k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
 k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
 k8s.io/kubectl v0.19.0/go.mod h1:gPCjjsmE6unJzgaUNXIFGZGafiUp5jh0If3F/x7/rRg=
 k8s.io/kubectl v0.19.4/go.mod h1:XPmlu4DJEYgD83pvZFeKF8+MSvGnYGqunbFSrJsqHv0=
@@ -2186,7 +2183,6 @@ sigs.k8s.io/structured-merge-diff v1.0.1/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8AP
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
diff --git a/pkg/cloudprovider/provider/anexia/provider.go b/pkg/cloudprovider/provider/anexia/provider.go
index 646882e2..796a8333 100644
--- a/pkg/cloudprovider/provider/anexia/provider.go
+++ b/pkg/cloudprovider/provider/anexia/provider.go
@@ -23,10 +23,18 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"time"
+
+	"github.com/anexia-it/go-anxcloud/pkg/vsphere/provisioning/progress"
+	"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/utils"
+	"k8s.io/apimachinery/pkg/api/meta"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/klog"
 
 	anxclient "github.com/anexia-it/go-anxcloud/pkg/client"
 	anxaddr "github.com/anexia-it/go-anxcloud/pkg/ipam/address"
-	anxvsphere "github.com/anexia-it/go-anxcloud/pkg/vsphere"
+	"github.com/anexia-it/go-anxcloud/pkg/vsphere"
 	anxvm "github.com/anexia-it/go-anxcloud/pkg/vsphere/provisioning/vm"
 
 	"github.com/kubermatic/machine-controller/pkg/apis/cluster/common"
@@ -43,25 +51,239 @@ import (
 	k8stypes "k8s.io/apimachinery/pkg/types"
 )
 
-type Config struct {
-	Token      string
-	VlanID     string
-	LocationID string
-	TemplateID string
-	CPUs       int
-	Memory     int
-	DiskSize   int
-}
+const (
+	ProvisionedType = "Provisioned"
+)
 
 type provider struct {
 	configVarResolver *providerconfig.ConfigVarResolver
 }
 
-func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*Config, *providerconfigtypes.Config, error) {
+func (p *provider) Create(machine *clusterv1alpha1.Machine, data *cloudprovidertypes.ProviderData,
+	userdata string, networkConfig *cloudprovidertypes.NetworkConfig) (instance instance.Instance, retErr error) {
+	status := getProviderStatus(machine)
+	klog.V(3).Infof(fmt.Sprintf("'%s' has status %#v", machine.Name, status))
+
+	// ensure conditions are present on machine
+	ensureConditions(&status)
+
+	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
+	if err != nil {
+		return nil, fmt.Errorf("unable to get provider config: %w", err)
+	}
+
+	ctx := utils.CreateReconcileContext(utils.ReconcileContext{
+		Status:       &status,
+		UserData:     userdata,
+		Config:       config,
+		ProviderData: data,
+		Machine:      machine,
+	})
+
+	client, err := getClient(config.Token)
+	if err != nil {
+		return nil, err
+	}
+
+	// make sure status is reflected in Machine Object
+	defer func() {
+		// if error occurs during updating the machine object don't override the original error
+		retErr = anxtypes.NewMultiError(retErr, updateMachineStatus(machine, status, data.Update))
+	}()
+
+	// check whether machine is already provisioning
+	if isAlreadyProvisioning(ctx) && status.ProvisioningID == "" {
+		klog.Info("ongoing provisioning detected")
+		err := waitForVM(ctx, client)
+		if err != nil {
+			return nil, err
+		}
+		return p.Get(machine, data)
+	}
+
+	// provision machine
+	err = provisionVM(ctx, client)
+	if err != nil {
+		return nil, err
+	}
+	return p.Get(machine, data)
+}
+
+func waitForVM(ctx context.Context, client anxclient.Client) error {
+	reconcileContext := utils.GetReconcileContext(ctx)
+	api := vsphere.NewAPI(client)
+	var identifier string
+	err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
+		klog.V(2).Info("checking for VM with name ", reconcileContext.Machine.Name)
+		vms, err := api.Search().ByName(ctx, fmt.Sprintf("%%-%s", reconcileContext.Machine.Name))
+		if err != nil {
+			return false, nil
+		}
+		if len(vms) < 1 {
+			return false, nil
+		}
+		if len(vms) > 1 {
+			return false, errors.New("too many VMs returned by search")
+		}
+		identifier = vms[0].Identifier
+		return true, nil
+	})
+	if err != nil {
+		return err
+	}
+
+	reconcileContext.Status.InstanceID = identifier
+	return updateMachineStatus(reconcileContext.Machine, *reconcileContext.Status, reconcileContext.ProviderData.Update)
+}
+
+func provisionVM(ctx context.Context, client anxclient.Client) error {
+	reconcileContext := utils.GetReconcileContext(ctx)
+	vmAPI := vsphere.NewAPI(client)
+
+	ctx, cancel := context.WithTimeout(ctx, anxtypes.CreateRequestTimeout)
+	defer cancel()
+
+	status := reconcileContext.Status
+	if status.ProvisioningID == "" {
+		klog.V(2).Info(fmt.Sprintf("Machine '%s'  does not contain a provisioningID yet. Starting to provision",
+			reconcileContext.Machine.Name))
+
+		config := reconcileContext.Config
+		reservedIP, err := getIPAddress(ctx, client)
+		if err != nil {
+			return newError(common.CreateMachineError, "failed to reserve IP: %v", err)
+		}
+		networkInterfaces := []anxvm.Network{{
+			NICType: anxtypes.VmxNet3NIC,
+			IPs:     []string{reservedIP},
+			VLAN:    config.VlanID,
+		}}
+
+		vm := vmAPI.Provisioning().VM().NewDefinition(
+			config.LocationID,
+			"templates",
+			config.TemplateID,
+			reconcileContext.Machine.Name,
+			config.CPUs,
+			config.Memory,
+			config.DiskSize,
+			networkInterfaces,
+		)
+
+		vm.Script = base64.StdEncoding.EncodeToString([]byte(reconcileContext.UserData))
+
+		sshKey, err := ssh.NewKey()
+		if err != nil {
+			return newError(common.CreateMachineError, "failed to generate ssh key: %v", err)
+		}
+		vm.SSH = sshKey.PublicKey
+
+		provisionResponse, err := vmAPI.Provisioning().VM().Provision(ctx, vm, false)
+		meta.SetStatusCondition(&status.Conditions, v1.Condition{
+			Type:    ProvisionedType,
+			Status:  v1.ConditionFalse,
+			Reason:  "Provisioning",
+			Message: "provisioning request was sent",
+		})
+		if err != nil {
+			return newError(common.CreateMachineError, "instance provisioning failed: %v", err)
+		}
+
+		// we successfully sent a VM provisioning request to the API, we consider the IP as 'Bound' now
+		status.IPState = anxtypes.IPStateBound
+
+		status.ProvisioningID = provisionResponse.Identifier
+		err = updateMachineStatus(reconcileContext.Machine, *status, reconcileContext.ProviderData.Update)
+		if err != nil {
+			return err
+		}
+	}
+
+	klog.V(2).Info(fmt.Sprintf("Using provisionID from machine '%s' to await completion",
+		reconcileContext.Machine.Name))
+
+	instanceID, err := vmAPI.Provisioning().Progress().AwaitCompletion(ctx, status.ProvisioningID)
+	if err != nil {
+		klog.Errorf("failed to await machine completion '%s'", reconcileContext.Machine.Name)
+		// something went wrong remove provisioning ID, so we can start from scratch
+		status.ProvisioningID = ""
+		return newError(common.CreateMachineError, "instance provisioning failed: %v", err)
+	}
+
+	status.InstanceID = instanceID
+	meta.SetStatusCondition(&status.Conditions, v1.Condition{
+		Type:    ProvisionedType,
+		Status:  v1.ConditionTrue,
+		Reason:  "Provisioned",
+		Message: "Machine has been successfully created",
+	})
+
+	return updateMachineStatus(reconcileContext.Machine, *status, reconcileContext.ProviderData.Update)
+}
+
+func getIPAddress(ctx context.Context, client anxclient.Client) (string, error) {
+	reconcileContext := utils.GetReconcileContext(ctx)
+	status := reconcileContext.Status
+
+	// only use IP if it is still unbound
+	if status.ReservedIP != "" && status.IPState == anxtypes.IPStateUnbound {
+		klog.Info("reusing already provisioned ip", "IP", status.ReservedIP)
+		return status.ReservedIP, nil
+	}
+	klog.Info(fmt.Sprintf("Creating a new IP for machine ''%s", reconcileContext.Machine.Name))
+	addrAPI := anxaddr.NewAPI(client)
+	config := reconcileContext.Config
+	res, err := addrAPI.ReserveRandom(ctx, anxaddr.ReserveRandom{
+		LocationID: config.LocationID,
+		VlanID:     config.VlanID,
+		Count:      1,
+	})
+	if err != nil {
+		return "", newError(common.InvalidConfigurationMachineError, "failed to reserve an ip address: %v", err)
+	}
+	if len(res.Data) < 1 {
+		return "", newError(common.InsufficientResourcesMachineError, "no ip address is available for this machine")
+	}
+
+	ip := res.Data[0].Address
+	status.ReservedIP = ip
+	status.IPState = anxtypes.IPStateUnbound
+
+	return ip, nil
+}
+
+func isAlreadyProvisioning(ctx context.Context) bool {
+	status := utils.GetReconcileContext(ctx).Status
+	condition := meta.FindStatusCondition(status.Conditions, ProvisionedType)
+	lastChange := condition.LastTransitionTime.Time
+	const reasonInProvisioning = "InProvisioning"
+	if condition.Reason == reasonInProvisioning && time.Since(lastChange) > 5*time.Minute {
+		meta.SetStatusCondition(&status.Conditions, v1.Condition{
+			Type:    ProvisionedType,
+			Reason:  "ReInitialising",
+			Message: "Could not find ongoing VM provisioning",
+			Status:  v1.ConditionFalse,
+		})
+	}
+
+	return condition.Status == v1.ConditionFalse && condition.Reason == reasonInProvisioning
+}
+
+func ensureConditions(status *anxtypes.ProviderStatus) {
+	conditions := [...]v1.Condition{
+		{Type: ProvisionedType, Message: "", Status: v1.ConditionUnknown, Reason: "Initialising"},
+	}
+	for _, condition := range conditions {
+		if meta.FindStatusCondition(status.Conditions, condition.Type) == nil {
+			meta.SetStatusCondition(&status.Conditions, condition)
+		}
+	}
+}
+
+func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*anxtypes.Config, *providerconfigtypes.Config, error) {
 	if provSpec.Value == nil {
 		return nil, nil, fmt.Errorf("machine.spec.providerSpec.value is nil")
 	}
-
 	pconfig, err := providerconfigtypes.GetConfig(provSpec)
 	if err != nil {
 		return nil, nil, err
@@ -76,7 +298,7 @@ func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*Config, *p
 		return nil, nil, err
 	}
 
-	c := Config{}
+	c := anxtypes.Config{}
 	c.Token, err = p.configVarResolver.GetConfigVarStringValueOrEnv(rawConfig.Token, anxtypes.AnxTokenEnv)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to get 'token': %v", err)
@@ -162,9 +384,9 @@ func (p *provider) Get(machine *clusterv1alpha1.Machine, _ *cloudprovidertypes.P
 	if err != nil {
 		return nil, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
 	}
-	vsphere := anxvsphere.NewAPI(cli)
+	vsphereAPI := vsphere.NewAPI(cli)
 
-	status, err := getStatus(machine.Status.ProviderStatus)
+	status := getProviderStatus(machine)
 	if err != nil {
 		return nil, newError(common.InvalidConfigurationMachineError, "failed to get machine status: %v", err)
 	}
@@ -175,7 +397,7 @@ func (p *provider) Get(machine *clusterv1alpha1.Machine, _ *cloudprovidertypes.P
 	ctx, cancel := context.WithTimeout(context.Background(), anxtypes.GetRequestTimeout)
 	defer cancel()
 
-	info, err := vsphere.Info().Get(ctx, status.InstanceID)
+	info, err := vsphereAPI.Info().Get(ctx, status.InstanceID)
 	if err != nil {
 		return nil, fmt.Errorf("failed get machine info: %w", err)
 	}
@@ -185,151 +407,99 @@ func (p *provider) Get(machine *clusterv1alpha1.Machine, _ *cloudprovidertypes.P
 	}, nil
 }
 
-func (p *provider) GetCloudConfig(spec clusterv1alpha1.MachineSpec) (string, string, error) {
+func (p *provider) GetCloudConfig(_ clusterv1alpha1.MachineSpec) (string, string, error) {
 	return "", "", nil
 }
 
-// Create creates a cloud instance according to the given machine
-func (p *provider) Create(machine *clusterv1alpha1.Machine, data *cloudprovidertypes.ProviderData, userdata string, networkConfig *cloudprovidertypes.NetworkConfig) (instance.Instance, error) {
+func (p *provider) Cleanup(machine *clusterv1alpha1.Machine, data *cloudprovidertypes.ProviderData) (isDeleted bool, retErr error) {
+	status := getProviderStatus(machine)
+	// make sure status is reflected in Machine Object
+	defer func() {
+		// if error occurs during updating the machine object don't override the original error
+		retErr = anxtypes.NewMultiError(retErr, updateMachineStatus(machine, status, data.Update))
+	}()
+
+	ensureConditions(&status)
 	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
 	if err != nil {
-		return nil, newError(common.InvalidConfigurationMachineError, "failed to parse MachineSpec: %v", err)
+		return false, newError(common.InvalidConfigurationMachineError, "failed to parse MachineSpec: %v", err)
 	}
 
 	cli, err := getClient(config.Token)
 	if err != nil {
-		return nil, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
+		return false, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
 	}
-	vsphere := anxvsphere.NewAPI(cli)
-	addr := anxaddr.NewAPI(cli)
-
-	ctx, cancel := context.WithTimeout(context.Background(), anxtypes.CreateRequestTimeout)
-	defer cancel()
+	vsphereAPI := vsphere.NewAPI(cli)
 
-	status, err := getStatus(machine.Status.ProviderStatus)
 	if err != nil {
-		return nil, newError(common.InvalidConfigurationMachineError, "failed to get machine status: %v", err)
+		return false, newError(common.InvalidConfigurationMachineError, "failed to get machine status: %v", err)
 	}
 
-	if status.ProvisioningID == "" {
-		res, err := addr.ReserveRandom(ctx, anxaddr.ReserveRandom{
-			LocationID: config.LocationID,
-			VlanID:     config.VlanID,
-			Count:      1,
-		})
-		if err != nil {
-			return nil, newError(common.InvalidConfigurationMachineError, "failed to reserve an ip address: %v", err)
-		}
-		if len(res.Data) < 1 {
-			return nil, newError(common.InsufficientResourcesMachineError, "no ip address is available for this machine")
-		}
-
-		networkInterfaces := []anxvm.Network{{
-			NICType: anxtypes.VmxNet3NIC,
-			IPs:     []string{res.Data[0].Address},
-			VLAN:    config.VlanID,
-		}}
-
-		vm := vsphere.Provisioning().VM().NewDefinition(
-			config.LocationID,
-			"templates",
-			config.TemplateID,
-			machine.ObjectMeta.Name,
-			config.CPUs,
-			config.Memory,
-			config.DiskSize,
-			networkInterfaces,
-		)
-
-		vm.Script = base64.StdEncoding.EncodeToString([]byte(userdata))
-
-		sshKey, err := ssh.NewKey()
-		if err != nil {
-			return nil, newError(common.CreateMachineError, "failed to generate ssh key: %v", err)
-		}
-		vm.SSH = sshKey.PublicKey
+	ctx, cancel := context.WithTimeout(context.Background(), anxtypes.DeleteRequestTimeout)
+	defer cancel()
 
-		provisionResponse, err := vsphere.Provisioning().VM().Provision(ctx, vm)
+	// first check whether there is an provisioning ongoing
+	if status.DeprovisioningID == "" {
+		response, err := vsphereAPI.Provisioning().VM().Deprovision(ctx, status.InstanceID, false)
 		if err != nil {
-			return nil, newError(common.CreateMachineError, "instance provisioning failed: %v", err)
-		}
-
-		status.ProvisioningID = provisionResponse.Identifier
-		if err := updateStatus(machine, status, data.Update); err != nil {
-			return nil, newError(common.UpdateMachineError, "machine status update failed: %v", err)
+			var respErr *anxclient.ResponseError
+			// Only error if the error was not "not found"
+			if !(errors.As(err, &respErr) && respErr.ErrorData.Code == http.StatusNotFound) {
+				return false, newError(common.DeleteMachineError, "failed to delete machine: %v", err)
+			}
 		}
+		status.DeprovisioningID = response.Identifier
 	}
 
-	instanceID, err := vsphere.Provisioning().Progress().AwaitCompletion(ctx, status.ProvisioningID)
-	if err != nil {
-		return nil, newError(common.CreateMachineError, "instance provisioning failed: %v", err)
-	}
-
-	status.InstanceID = instanceID
-	if err := updateStatus(machine, status, data.Update); err != nil {
-		return nil, newError(common.UpdateMachineError, "machine status update failed: %v", err)
-	}
-
-	return p.Get(machine, data)
+	return isTaskDone(ctx, cli, status.DeprovisioningID)
 }
 
-func (p *provider) Cleanup(machine *clusterv1alpha1.Machine, _ *cloudprovidertypes.ProviderData) (bool, error) {
-	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
-	if err != nil {
-		return false, newError(common.InvalidConfigurationMachineError, "failed to parse MachineSpec: %v", err)
-	}
-
-	cli, err := getClient(config.Token)
+func isTaskDone(ctx context.Context, cli anxclient.Client, progressIdentifier string) (bool, error) {
+	response, err := progress.NewAPI(cli).Get(ctx, progressIdentifier)
 	if err != nil {
-		return false, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
+		return false, err
 	}
-	vsphere := anxvsphere.NewAPI(cli)
 
-	status, err := getStatus(machine.Status.ProviderStatus)
-	if err != nil {
-		return false, newError(common.InvalidConfigurationMachineError, "failed to get machine status: %v", err)
+	if len(response.Errors) != 0 {
+		taskErrors, _ := json.Marshal(response.Errors)
+		return true, fmt.Errorf("task failed with: %s", taskErrors)
 	}
 
-	ctx, cancel := context.WithTimeout(context.Background(), anxtypes.DeleteRequestTimeout)
-	defer cancel()
-
-	err = vsphere.Provisioning().VM().Deprovision(ctx, status.InstanceID, false)
-	if err != nil {
-		var respErr *anxclient.ResponseError
-		// Only error if the error was not "not found"
-		if !(errors.As(err, &respErr) && respErr.ErrorData.Code == http.StatusNotFound) {
-			return false, newError(common.DeleteMachineError, "failed to delete machine: %v", err)
-		}
+	if response.Progress == 100 {
+		return true, nil
 	}
 
-	return true, nil
+	return false, nil
 }
 
 func (p *provider) MigrateUID(_ *clusterv1alpha1.Machine, _ k8stypes.UID) error {
 	return nil
 }
 
-func (p *provider) MachineMetricsLabels(machine *clusterv1alpha1.Machine) (map[string]string, error) {
+func (p *provider) MachineMetricsLabels(_ *clusterv1alpha1.Machine) (map[string]string, error) {
 	return map[string]string{}, nil
 }
 
-func (p *provider) SetMetricsForMachines(machine clusterv1alpha1.MachineList) error {
+func (p *provider) SetMetricsForMachines(_ clusterv1alpha1.MachineList) error {
 	return nil
 }
 
 func getClient(token string) (anxclient.Client, error) {
 	tokenOpt := anxclient.TokenFromString(token)
-	return anxclient.New(tokenOpt)
+	client := anxclient.HTTPClient(&http.Client{Timeout: 30 * time.Second})
+	return anxclient.New(tokenOpt, client)
 }
 
-func getStatus(rawStatus *runtime.RawExtension) (*anxtypes.ProviderStatus, error) {
-	var status anxtypes.ProviderStatus
-	if rawStatus != nil && rawStatus.Raw != nil {
-		if err := json.Unmarshal(rawStatus.Raw, &status); err != nil {
-			return nil, err
+func getProviderStatus(machine *clusterv1alpha1.Machine) anxtypes.ProviderStatus {
+	var providerStatus anxtypes.ProviderStatus
+	status := machine.Status.ProviderStatus
+	if status != nil && status.Raw != nil {
+		if err := json.Unmarshal(status.Raw, &providerStatus); err != nil {
+			klog.Warningf("Unable to parse status from machine object. status was discarded for machine")
+			return anxtypes.ProviderStatus{}
 		}
 	}
-	return &status, nil
+	return providerStatus
 }
 
 // newError creates a terminal error matching to the provider interface.
@@ -340,7 +510,9 @@ func newError(reason common.MachineStatusError, msg string, args ...interface{})
 	}
 }
 
-func updateStatus(machine *clusterv1alpha1.Machine, status *anxtypes.ProviderStatus, updater cloudprovidertypes.MachineUpdater) error {
+// updateMachineStatus tries to update the machine status by any means
+// an error will lead to a panic
+func updateMachineStatus(machine *clusterv1alpha1.Machine, status anxtypes.ProviderStatus, updater cloudprovidertypes.MachineUpdater) error {
 	rawStatus, err := json.Marshal(status)
 	if err != nil {
 		return err
@@ -350,6 +522,7 @@ func updateStatus(machine *clusterv1alpha1.Machine, status *anxtypes.ProviderSta
 			Raw: rawStatus,
 		}
 	})
+
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cloudprovider/provider/anexia/types/errors.go b/pkg/cloudprovider/provider/anexia/types/errors.go
new file mode 100644
index 00000000..6e73d232
--- /dev/null
+++ b/pkg/cloudprovider/provider/anexia/types/errors.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 The Machine Controller Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package types
+
+import (
+	"fmt"
+	"strings"
+)
+
+// MultiError represent multiple errors at the same time.
+type MultiError []error
+
+func (r MultiError) Error() string {
+	errString := make([]string, len(r))
+	for i, err := range r {
+		errString[i] = fmt.Sprintf("Error %d: %s", i, err)
+	}
+	return fmt.Sprintf("Multiple errors occoured:\n%s", strings.Join(errString, "\n"))
+}
+
+func NewMultiError(errs ...error) error {
+	var combinedErr []error
+	for _, err := range errs {
+		if err == nil {
+			continue
+		}
+		combinedErr = append(combinedErr, err)
+	}
+
+	if len(combinedErr) > 0 {
+		return MultiError(combinedErr)
+	}
+
+	return nil
+}
diff --git a/pkg/cloudprovider/provider/anexia/types/types.go b/pkg/cloudprovider/provider/anexia/types/types.go
index 9bdc929f..1046315f 100644
--- a/pkg/cloudprovider/provider/anexia/types/types.go
+++ b/pkg/cloudprovider/provider/anexia/types/types.go
@@ -17,6 +17,9 @@ limitations under the License.
 package types
 
 import (
+	"github.com/kubermatic/machine-controller/pkg/apis/cluster/common"
+	cloudprovidererrors "github.com/kubermatic/machine-controller/pkg/cloudprovider/errors"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"time"
 
 	"github.com/kubermatic/machine-controller/pkg/jsonutil"
@@ -30,10 +33,18 @@ const (
 	GetRequestTimeout    = 1 * time.Minute
 	DeleteRequestTimeout = 1 * time.Minute
 
+	IPStateBound   = "Bound"
+	IPStateUnbound = "Unbound"
+
 	VmxNet3NIC       = "vmxnet3"
 	MachinePoweredOn = "poweredOn"
 )
 
+var StatusUpdateFailed = cloudprovidererrors.TerminalError{
+	Reason:  common.UpdateMachineError,
+	Message: "Unable to update the machine status",
+}
+
 type RawConfig struct {
 	Token      providerconfigtypes.ConfigVarString `json:"token,omitempty"`
 	VlanID     providerconfigtypes.ConfigVarString `json:"vlanID"`
@@ -45,9 +56,22 @@ type RawConfig struct {
 }
 
 type ProviderStatus struct {
-	InstanceID     string `json:"instanceID"`
-	ProvisioningID string `json:"provisioningID"`
-	// TODO: add conditions to track progress on the provider side
+	InstanceID       string         `json:"instanceID"`
+	ProvisioningID   string         `json:"provisioningID"`
+	DeprovisioningID string         `json:"deprovisioningID"`
+	ReservedIP       string         `json:"reservedIP"`
+	IPState          string         `json:"ipState"`
+	Conditions       []v1.Condition `json:"conditions,omitempty"`
+}
+
+type Config struct {
+	Token      string
+	VlanID     string
+	LocationID string
+	TemplateID string
+	CPUs       int
+	Memory     int
+	DiskSize   int
 }
 
 func GetConfig(pconfig providerconfigtypes.Config) (*RawConfig, error) {
diff --git a/pkg/cloudprovider/provider/anexia/utils/utils.go b/pkg/cloudprovider/provider/anexia/utils/utils.go
new file mode 100644
index 00000000..97aa73d5
--- /dev/null
+++ b/pkg/cloudprovider/provider/anexia/utils/utils.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 The Machine Controller Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+	"context"
+
+	"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
+	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
+	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
+)
+
+type contextKey byte
+
+const MachineReconcileContextKey contextKey = 0
+
+type ReconcileContext struct {
+	Machine      *v1alpha1.Machine
+	Status       *anxtypes.ProviderStatus
+	UserData     string
+	Config       *anxtypes.Config
+	ProviderData *cloudprovidertypes.ProviderData
+}
+
+func CreateReconcileContext(cc ReconcileContext) context.Context {
+	return context.WithValue(context.Background(), MachineReconcileContextKey, cc)
+}
+
+func GetReconcileContext(ctx context.Context) ReconcileContext {
+	rawContext := ctx.Value(MachineReconcileContextKey)
+	if recContext, ok := rawContext.(ReconcileContext); ok {
+		return recContext
+	}
+	return ReconcileContext{}
+}
diff --git a/pkg/cloudprovider/provider/kubevirt/provider.go b/pkg/cloudprovider/provider/kubevirt/provider.go
index 1b69254c..af171fbe 100644
--- a/pkg/cloudprovider/provider/kubevirt/provider.go
+++ b/pkg/cloudprovider/provider/kubevirt/provider.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/url"
 	"os"
 	"strconv"
 	"strings"
@@ -34,8 +35,10 @@ import (
 	"github.com/kubermatic/machine-controller/pkg/cloudprovider/instance"
 	kubevirttypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/kubevirt/types"
 	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
+	controllerutil "github.com/kubermatic/machine-controller/pkg/controller/util"
 	"github.com/kubermatic/machine-controller/pkg/providerconfig"
 	providerconfigtypes "github.com/kubermatic/machine-controller/pkg/providerconfig/types"
+
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -46,7 +49,6 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/klog"
 	utilpointer "k8s.io/utils/pointer"
-	"net/url"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -373,6 +375,10 @@ func (p *provider) Create(machine *clusterv1alpha1.Machine, data *cloudprovidert
 
 	resourceRequirements := kubevirtv1.ResourceRequirements{}
 	labels := map[string]string{"kubevirt.io/vm": machine.Name}
+	// Add a common label to all VirtualMachines spawned by the same MachineDeployment (= MachineDeployment name)
+	if mdName, err := controllerutil.GetMachineDeploymentNameForMachine(context.Background(), machine, data.Client); err == nil {
+		labels["md"] = mdName
+	}
 
 	sigClient, err := client.New(c.RestConfig, client.Options{})
 	if err != nil {


Test patch:
diff --git a/pkg/cloudprovider/provider/anexia/helper_test.go b/pkg/cloudprovider/provider/anexia/helper_test.go
new file mode 100644
index 00000000..2edc3ba3
--- /dev/null
+++ b/pkg/cloudprovider/provider/anexia/helper_test.go
@@ -0,0 +1,91 @@
+/*
+Copyright 2022 The Machine Controller Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package anexia
+
+import (
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/anexia-it/go-anxcloud/pkg/vsphere/search"
+	"github.com/gophercloud/gophercloud/testhelper"
+	"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
+	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
+	"github.com/kubermatic/machine-controller/pkg/providerconfig/types"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+type ConfigTestCase struct {
+	Config anxtypes.RawConfig
+	Error  error
+}
+
+type ValidateCallTestCase struct {
+	Spec          v1alpha1.MachineSpec
+	ExpectedError error
+}
+
+func getSpecsForValidationTest(t *testing.T, configCases []ConfigTestCase) []ValidateCallTestCase {
+
+	var testCases []ValidateCallTestCase
+
+	for _, configCase := range configCases {
+		jsonConfig, err := json.Marshal(configCase.Config)
+		testhelper.AssertNoErr(t, err)
+		jsonProviderConfig, err := json.Marshal(types.Config{
+			CloudProviderSpec:   runtime.RawExtension{Raw: jsonConfig},
+			OperatingSystemSpec: runtime.RawExtension{Raw: []byte("{}")},
+		})
+		testhelper.AssertNoErr(t, err)
+		testCases = append(testCases, ValidateCallTestCase{
+			Spec: v1alpha1.MachineSpec{
+				ProviderSpec: v1alpha1.ProviderSpec{
+					Value: &runtime.RawExtension{Raw: jsonProviderConfig},
+				},
+			},
+			ExpectedError: configCase.Error,
+		})
+	}
+	return testCases
+}
+
+func createSearchHandler(t *testing.T, iterations int) http.HandlerFunc {
+	counter := 0
+	return func(writer http.ResponseWriter, request *http.Request) {
+		test := request.URL.Query().Get("name")
+		testhelper.AssertEquals(t, "%-TestMachine", test)
+		testhelper.TestMethod(t, request, http.MethodGet)
+		if iterations == counter {
+			encoder := json.NewEncoder(writer)
+			testhelper.AssertNoErr(t, encoder.Encode(map[string]interface{}{
+				"data": []search.VM{
+					{
+						Name:       "543053-TestMachine",
+						Identifier: TestIdentifier,
+					},
+				},
+			}))
+		}
+		counter++
+	}
+}
+
+func newConfigVarString(str string) types.ConfigVarString {
+	return types.ConfigVarString{
+		Value: str,
+	}
+}
diff --git a/pkg/cloudprovider/provider/anexia/provider_test.go b/pkg/cloudprovider/provider/anexia/provider_test.go
new file mode 100644
index 00000000..4fe74670
--- /dev/null
+++ b/pkg/cloudprovider/provider/anexia/provider_test.go
@@ -0,0 +1,333 @@
+/*
+Copyright 2022 The Machine Controller Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package anexia
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"testing"
+	"time"
+
+	anxclient "github.com/anexia-it/go-anxcloud/pkg/client"
+	"github.com/anexia-it/go-anxcloud/pkg/ipam/address"
+	"github.com/anexia-it/go-anxcloud/pkg/vsphere/provisioning/progress"
+	"github.com/anexia-it/go-anxcloud/pkg/vsphere/provisioning/vm"
+	"github.com/gophercloud/gophercloud/testhelper"
+	"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
+	clusterv1alpha1 "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
+	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
+	"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/utils"
+	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+const TestIdentifier = "TestIdent"
+
+func TestAnexiaProvider(t *testing.T) {
+	testhelper.SetupHTTP()
+	client, server := anxclient.NewTestClient(nil, testhelper.Mux)
+	t.Cleanup(func() {
+		testhelper.TeardownHTTP()
+		server.Close()
+	})
+
+	t.Run("Test waiting for VM", func(t *testing.T) {
+		t.Parallel()
+
+		waitUntilVMIsFound := 2
+		testhelper.Mux.HandleFunc("/api/vsphere/v1/search/by_name.json", createSearchHandler(t, waitUntilVMIsFound))
+
+		providerStatus := anxtypes.ProviderStatus{}
+		ctx := utils.CreateReconcileContext(utils.ReconcileContext{
+			Machine: &v1alpha1.Machine{
+				ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
+			},
+			Status:   &providerStatus,
+			UserData: "",
+			Config:   &anxtypes.Config{},
+
+			ProviderData: &cloudprovidertypes.ProviderData{
+				Update: func(m *clusterv1alpha1.Machine, mod ...cloudprovidertypes.MachineModifier) error {
+					return nil
+				},
+			},
+		})
+
+		err := waitForVM(ctx, client)
+		if err != nil {
+			t.Fatal("No error was expected", err)
+
+		}
+
+		if providerStatus.InstanceID != TestIdentifier {
+			t.Errorf("Excpected InstanceID to be set")
+		}
+	})
+
+	t.Run("Test provision VM", func(t *testing.T) {
+		t.Parallel()
+		testhelper.Mux.HandleFunc("/api/ipam/v1/address/reserve/ip/count.json", func(writer http.ResponseWriter, request *http.Request) {
+			err := json.NewEncoder(writer).Encode(address.ReserveRandomSummary{
+				Data: []address.ReservedIP{
+					{
+						ID:      "IP-ID",
+						Address: "8.8.8.8",
+					},
+				},
+			})
+			testhelper.AssertNoErr(t, err)
+		})
+
+		testhelper.Mux.HandleFunc("/api/vsphere/v1/provisioning/vm.json/LOCATION-ID/templates/TEMPLATE-ID", func(writer http.ResponseWriter, request *http.Request) {
+			testhelper.TestMethod(t, request, http.MethodPost)
+			type jsonObject = map[string]interface{}
+			expectedJSON := map[string]interface{}{
+				"cpu_performance_type": "performance",
+				"hostname":             "TestMachine",
+				"memory_mb":            json.Number("5"),
+				"network": []jsonObject{
+					{
+						"vlan":     "VLAN-ID",
+						"nic_type": "vmxnet3",
+						"ips":      []interface{}{"8.8.8.8"},
+					},
+				},
+			}
+			var jsonBody jsonObject
+			decoder := json.NewDecoder(request.Body)
+			decoder.UseNumber()
+			testhelper.AssertNoErr(t, decoder.Decode(&jsonBody))
+			testhelper.AssertEquals(t, expectedJSON["cpu_performance_type"], jsonBody["cpu_performance_type"])
+			testhelper.AssertEquals(t, expectedJSON["hostname"], jsonBody["hostname"])
+			testhelper.AssertEquals(t, expectedJSON["memory_mb"], jsonBody["memory_mb"])
+			testhelper.AssertEquals(t, expectedJSON["count"], jsonBody["count"])
+
+			expectedNetwork := expectedJSON["network"].([]jsonObject)[0]
+			bodyNetwork := jsonBody["network"].([]interface{})[0].(jsonObject)
+			testhelper.AssertEquals(t, expectedNetwork["vlan"], bodyNetwork["vlan"])
+			testhelper.AssertEquals(t, expectedNetwork["nic_type"], bodyNetwork["nic_type"])
+			testhelper.AssertEquals(t, expectedNetwork["ips"].([]interface{})[0], bodyNetwork["ips"].([]interface{})[0])
+
+			err := json.NewEncoder(writer).Encode(vm.ProvisioningResponse{
+				Progress:   100,
+				Errors:     nil,
+				Identifier: "TEST-IDENTIFIER",
+				Queued:     false,
+			})
+			testhelper.AssertNoErr(t, err)
+		})
+
+		testhelper.Mux.HandleFunc("/api/vsphere/v1/provisioning/progress.json/TEST-IDENTIFIER", func(writer http.ResponseWriter, request *http.Request) {
+			testhelper.TestMethod(t, request, http.MethodGet)
+
+			err := json.NewEncoder(writer).Encode(progress.Progress{
+				TaskIdentifier: "TEST-IDENTIFIER",
+				Queued:         false,
+				Progress:       100,
+				VMIdentifier:   "VM-IDENTIFIER",
+				Errors:         nil,
+			})
+			testhelper.AssertNoErr(t, err)
+		})
+
+		providerStatus := anxtypes.ProviderStatus{}
+		ctx := utils.CreateReconcileContext(utils.ReconcileContext{
+			Machine: &v1alpha1.Machine{
+				ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
+			},
+			Status:   &providerStatus,
+			UserData: "",
+			Config: &anxtypes.Config{
+				VlanID:     "VLAN-ID",
+				LocationID: "LOCATION-ID",
+				TemplateID: "TEMPLATE-ID",
+				CPUs:       5,
+				Memory:     5,
+				DiskSize:   5,
+			},
+			ProviderData: &cloudprovidertypes.ProviderData{
+				Update: func(m *clusterv1alpha1.Machine, mods ...cloudprovidertypes.MachineModifier) error {
+					return nil
+				},
+			},
+		})
+
+		err := provisionVM(ctx, client)
+		testhelper.AssertNoErr(t, err)
+	})
+
+	t.Run("Test is VM Provisioning", func(t *testing.T) {
+		t.Parallel()
+		providerStatus := anxtypes.ProviderStatus{
+			Conditions: []metav1.Condition{
+				{
+					Type:   ProvisionedType,
+					Reason: "InProvisioning",
+					Status: metav1.ConditionFalse,
+				},
+			},
+		}
+		ctx := utils.CreateReconcileContext(utils.ReconcileContext{
+			Status:       &providerStatus,
+			UserData:     "",
+			Config:       nil,
+			ProviderData: nil,
+		})
+
+		condition := meta.FindStatusCondition(providerStatus.Conditions, ProvisionedType)
+		condition.LastTransitionTime = metav1.Time{Time: time.Now().Add(-1 * time.Minute)}
+		testhelper.AssertEquals(t, true, isAlreadyProvisioning(ctx))
+
+		condition.Reason = "Provisioned"
+		condition.Status = metav1.ConditionTrue
+		testhelper.AssertEquals(t, false, isAlreadyProvisioning(ctx))
+
+		condition.Reason = "InProvisioning"
+		condition.Status = metav1.ConditionFalse
+		condition.LastTransitionTime = metav1.Time{Time: time.Now().Add(-10 * time.Minute)}
+		testhelper.AssertEquals(t, false, isAlreadyProvisioning(ctx))
+		testhelper.AssertEquals(t, condition.Reason, "ReInitialising")
+	})
+
+	t.Run("Test getIPAddress", func(t *testing.T) {
+		t.Parallel()
+		providerStatus := &anxtypes.ProviderStatus{
+			ReservedIP: "",
+			IPState:    "",
+		}
+		ctx := utils.CreateReconcileContext(utils.ReconcileContext{Status: providerStatus})
+
+		t.Run("with unbound reserved IP", func(t *testing.T) {
+			expectedIP := "8.8.8.8"
+			providerStatus.ReservedIP = expectedIP
+			providerStatus.IPState = anxtypes.IPStateUnbound
+			reservedIP, err := getIPAddress(ctx, client)
+			testhelper.AssertNoErr(t, err)
+			testhelper.AssertEquals(t, expectedIP, reservedIP)
+		})
+	})
+}
+
+func TestValidate(t *testing.T) {
+	t.Parallel()
+
+	var configCases []ConfigTestCase
+	configCases = append(configCases,
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{},
+			Error:  errors.New("token is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN")},
+			Error:  errors.New("cpu count is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1},
+			Error:  errors.New("disk size is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5},
+			Error:  errors.New("memory size is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5},
+			Error:  errors.New("location id is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
+				LocationID: newConfigVarString("TLID")},
+			Error: errors.New("template id is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
+				LocationID: newConfigVarString("LID"), TemplateID: newConfigVarString("TID")},
+			Error: errors.New("vlan id is missing"),
+		},
+		ConfigTestCase{
+			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
+				LocationID: newConfigVarString("LID"), TemplateID: newConfigVarString("TID"), VlanID: newConfigVarString("VLAN")},
+			Error: nil,
+		},
+	)
+
+	provider := New(nil)
+	for _, testCase := range getSpecsForValidationTest(t, configCases) {
+		err := provider.Validate(testCase.Spec)
+		if testCase.ExpectedError != nil {
+			testhelper.AssertEquals(t, testCase.ExpectedError.Error(), err.Error())
+		} else {
+			testhelper.AssertEquals(t, testCase.ExpectedError, err)
+		}
+	}
+}
+
+func TestEnsureConditions(t *testing.T) {
+	t.Parallel()
+	status := anxtypes.ProviderStatus{}
+
+	ensureConditions(&status)
+
+	condition := meta.FindStatusCondition(status.Conditions, ProvisionedType)
+	if condition == nil {
+		t.Fatal("condition should not be nil")
+	}
+	testhelper.AssertEquals(t, metav1.ConditionUnknown, condition.Status)
+	testhelper.AssertEquals(t, "Initialising", condition.Reason)
+}
+
+func TestGetProviderStatus(t *testing.T) {
+	t.Parallel()
+
+	machine := &v1alpha1.Machine{}
+	providerStatus := anxtypes.ProviderStatus{
+		InstanceID: "InstanceID",
+	}
+	providerStatusJSON, err := json.Marshal(providerStatus)
+	testhelper.AssertNoErr(t, err)
+	machine.Status.ProviderStatus = &runtime.RawExtension{Raw: providerStatusJSON}
+
+	returnedStatus := getProviderStatus(machine)
+
+	testhelper.AssertEquals(t, "InstanceID", returnedStatus.InstanceID)
+
+}
+
+func TestUpdateStatus(t *testing.T) {
+	t.Parallel()
+	machine := &v1alpha1.Machine{}
+	providerStatus := anxtypes.ProviderStatus{
+		InstanceID: "InstanceID",
+	}
+	providerStatusJSON, err := json.Marshal(providerStatus)
+	testhelper.AssertNoErr(t, err)
+	machine.Status.ProviderStatus = &runtime.RawExtension{Raw: providerStatusJSON}
+
+	called := false
+	err = updateMachineStatus(machine, providerStatus, func(paramMachine *v1alpha1.Machine, modifier ...cloudprovidertypes.MachineModifier) error {
+		called = true
+		testhelper.AssertEquals(t, machine, paramMachine)
+		status := getProviderStatus(machine)
+		testhelper.AssertEquals(t, status.InstanceID, providerStatus.InstanceID)
+		return nil
+	})
+
+	testhelper.AssertEquals(t, true, called)
+	testhelper.AssertNoErr(t, err)
+}