Repository: syself/cluster-api-provider-hetzner
Pull request #1411 (2024-07-31 19:46:43)

🌱 Change order and add new article
**What this PR does / why we need it**:

Improves the README


Patch:
diff --git a/README.md b/README.md
index 67e1e145..7231add6 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,12 @@
 <div align="center">
 <a href="https://syself.com/docs/caph/getting-started/quickstart">Quickstart</a> |
 <a href="https://syself.com/docs/caph/getting-started/introduction">Docs</a> |
-<a href="https://syself.com/docs/caph/developers/development-guide">Contribution Guide</a><br><br>
-<a href="https://cluster-api.sigs.k8s.io/">Cluster API Book</a>
+<a href="https://cluster-api.sigs.k8s.io/">Cluster API Book</a><br><br>
+<p>⭐ Consider leaving a star — it motivates us a lot! ⭐</p>
 </div>
 
+
+
 ---
 
 <div align="center">
@@ -22,60 +24,43 @@
 
 <br>
 
-The Kubernetes Cluster API Provider Hetzner (CAPH) enables declarative provisioning of multiple Kubernetes clusters on [Hetzner infrastructure](https://hetzner.cloud).
+## Table of Contents
+- [What is CAPH](#-what-is-the-cluster-api-provider-hetzner)
+- [Documentation](#-documentation)
+- [Getting Started](#-getting-started)
+- [Version Compatibility](#%EF%B8%8F-compatibility-with-cluster-api-and-kubernetes-versions)
+- [Node Images](#-operating-system-images)
+- [Contributing](#-getting-involved-and-contributing)
+- [Contact](#-contact)
 
-With CAPH, you can manage highly-available Kubernetes clusters on both bare metal and cloud instances, leveraging the Cluster API to handle creation, updates, and operations of production-ready, self-managed Kubernetes clusters at any scale.
+## 📰 What is the Cluster API Provider Hetzner?
 
 > [!NOTE]
 > The Cluster API Provider Hetzner is independently maintained by [Syself](https://syself.com) and the community. It is not an official Hetzner project.
 >
 > If you have any questions about this project, please start a conversation in the [Discussions](https://github.com/syself/cluster-api-provider-hetzner/discussions) tab or contact us at [contact@syself.com](mailto:contact@syself.com?subject=cluster-api-provider-hetzner).
 
-## 📰 What is the Cluster API Provider Hetzner?
-
-The [Cluster API][cluster_api] orchestrates infrastructure similarly to how Kubernetes manages containers. It implements a declarative API like Kubernetes does and extends the resources of the Kubernetes API server via CRDs.
+The Cluster API Provider Hetzner (CAPH) provides a way to declaratively create and manage infrastructure on Hetzner, in a Kubernetes-native way. It extends the Kubernetes API with Custom Resource Definitions (CRDs) allowing you to interact with clusters in the same fashion you interact with workload.
 
-The Cluster API consists of the CAPI controller, the control-plane provider, the bootstrap provider, and an infrastructure provider like CAPH, that translates resources in Hetzner to objects in the Kubernetes API.
+Key benefits include:
 
-The controllers ensure that the desired state of the infrastructure is achieved - just as Kubernetes ensures the desired state of containers. The concept of [Kubernetes Controller](https://kubernetes.io/docs/concepts/architecture/controller/) has significant advantages over traditional Infrastructure as Code (IaC) solutions because it can react automatically to changes and problems. The best example of this is the MachineHealthCheck, which replaces unhealthy nodes automatically.
+- **Self-healing**: CAPH and CAPI controllers react to every change in your infrastructure, identifying and resolving issues without human intervention
+- **Declarative**: Specify the desired state of your infrastructure and let the operators do the rest, ensuring repeatability and idempotency
+- **Kubernetes native**: Everything is a Kubernetes resource, meaning you can use tools you're already familiar with while working with CAPH
 
-Using CAPH unites the benefits of declarative infrastructure, cost-effectiveness, and GDPR-compliant European cloud, ensuring that your clusters can automatically adapt to changes and problems.
+CAPH enables you to have DIY Kubernetes on Hetzner at any scale, with full control over your infrastructure and clusters configuration.
 
-## ✨ Features of CAPH
+If you want a batteries-included solution instead, you can try [Syself](https://syself.com) free for 14 days.
 
-- Native Kubernetes resources and API
-- Works with your choice of Linux distribution
-- Support for single and multi-node control plane clusters (HA Kubernetes)
-- Support for Hetzner Cloud placement groups, network, and load balancer
-- Complete day 2 operations - updating Kubernetes and nodes, scaling up and down, self-healing
-- Custom CSR approver for approving [kubelet-serving certificate signing requests](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/#kubelet-serving-certs)
-- Hetzner dedicated servers / bare metal (and GPUs)
-
-## 👀 Clarifying Scope
-
-Managing a production-grade Kubernetes system requires a **dedicated team of experts**.
-
-The Cluster API Provider Hetzner (CAPH) handles the lifecycle management of machines and infrastructure, but certain aspects need to be managed separately:
-
-- ❌ Production-ready node images
-- ❌ Secured kubeadm configuration
-- ❌ Incorporation of cluster add-ons, such as CNI (e.g. cilium), metrics-server, konnectivity-service, etc.
-- ❌ Testing & update procedures of Kubernetes version, configuration
-- ❌ Backup procedures
-- ❌ Monitoring strategies
-- ❌ Alerting systems
-- ❌ Identity and Access Management (IAM)
+## 📖 Documentation
 
-If you don't have a dedicated team for managing Kubernetes, you can use [Syself Autopilot](https://syself.com) and enjoy a wide range of benefits, including:
+Documentation can be found at [caph.syself.com](https://caph.syself.com). You can contribute to it by modifying the contents of the `/docs` directory.
 
-- ✅ Consistent, regular updates that provide you with the latest features and improvements.
-- ✅ Highly optimized defaults, reducing costs by up to 80% without performance impacts.
-- ✅ Production-ready clusters working out of the box.
-- ✅ Specialized expertise in Cluster API and Hetzner for quick issue resolution and 24/7 support.
+## 🚀 Getting Started
 
-## 🚀 Get Started
+The best way to get started with CAPH is to spin up a cluster. For that you can follow our [**Managing Kubernetes on Hetzner with Cluster API**](https://community.hetzner.com/tutorials/kubernetes-on-hetzner-with-cluster-api) article featured in the Hetzner Community Tutorials.
 
-Ready to dive in? Here are some resources to get you started:
+Additional resources from the documentation:
 
 - [**Cluster API Provider Hetzner 15 Minute Tutorial**](https://syself.com/docs/caph/getting-started/quickstart): Set up a bootstrap cluster using Kind and deploy a Kubernetes cluster on Hetzner.
 - [**Develop and test Kubernetes clusters with Tilt**](https://syself.com/docs/caph/developers/development-guide): Start using Tilt for rapid testing of various cluster flavors, like with/without a private network or bare metal.
@@ -83,6 +68,7 @@ Ready to dive in? Here are some resources to get you started:
 
 In addition to the pure creation and operation of Kubernetes clusters, this provider can also validate and approve certificate signing requests. This increases security as the kubelets of the nodes can be operated with signed certificates, and enables the metrics-server to run securely. [Click here](https://syself.com/docs/caph/topics/advanced/csr-controller) to read more about the CSR controller.
 
+
 ## 🖇️ Compatibility with Cluster API and Kubernetes Versions
 
 This provider's versions are compatible with the following versions of Cluster API:
@@ -90,7 +76,7 @@ This provider's versions are compatible with the following versions of Cluster A
 |                                   | Cluster API `v1beta1` (`v1.6.x`) | Cluster API `v1beta1` (`v1.7.x`) |
 | --------------------------------- | -------------------------------- | -------------------------------- |
 | Hetzner Provider `v1.0.0-beta.33` | ✅                              | ❌                               |
-| Hetzner Provider `v1.0.0-beta.34-35` | ❌                              | ✅                               |
+| Hetzner Provider `v1.0.0-beta.34-40` | ❌                              | ✅                               |
 
 This provider's versions can install and manage the following versions of Kubernetes:
 
@@ -114,45 +100,37 @@ Each version of Cluster API for Hetzner will attempt to support at least two Kub
 
 **NOTE:** As the versioning for this project is tied to the versioning of Cluster API, future modifications to this policy may be made to more closely align with other providers in the Cluster API ecosystem.
 
----
-
 ## 💿 Operating System Images
 
-> [!NOTE]
-> Cluster API Provider Hetzner relies on a few prerequisites that must be already installed in the operating system images, such as a container runtime, kubelet, and Kubeadm.
->
-> Reference images are available in kubernetes-sigs/image-builder and [templates/node-image](/templates/node-image).
->
-> If pre-installation of these prerequisites isn't possible, [custom scripts can be deployed](/docs/caph/02-topics/02-node-image) through the Kubeadm config.
+Cluster API Provider Hetzner relies on a few prerequisites that must be already installed in the operating system images, such as a container runtime, kubelet, and kubeadm.
 
----
+Reference images are available in kubernetes-sigs/image-builder and [templates/node-image](/templates/node-image).
 
-## 📖 Documentation
+If it's not possible to pre-install these prerequisites, [custom scripts can be deployed](/docs/caph/02-topics/02-node-image) through the kubeadm config.
 
-Documentation can be found at: [https://syself.com/docs/caph](https://syself.com/docs/caph)
+In case you want a solution with managed node images, [Syself](https://syself.com) might be interesting for you.
 
-## 👥 Getting Involved and Contributing
+## 🤝 Getting Involved and Contributing
 
-We, maintainers and the community, welcome any contributions to Cluster API Provider Hetzner. For suggestions, contributions, and assistance, contact the maintainers anytime.
+We, the maintainers and the community, welcome any contributions to Cluster API Provider Hetzner. Feel free to contact the maintainers for suggestions, contributions and help.
 
 To set up your environment, refer to the [development guide](https://syself.com/docs/caph/developers/development-guide).
 
 For new contributors, check out issues tagged as [`good first issue`][good_first_issue]. These are typically smaller in scope and great for getting familiar with the codebase.
 
-We encourage **all** active community participants to act as if they were maintainers, even without "official" write permissions. This is a community effort serving the Kubernetes community.
+We encourage **all** active community members to act as if they were maintainers, even without "official" write permissions. This is a collaborative effort serving the Kubernetes community.
 
 If you have an active interest and you want to get involved, you have real power! Don't assume that the only people who can get things done around here are the "maintainers".
 
-We would also love to add more "official" maintainers, so show us what you can
-do!
+We would also love to add more "official" maintainers, so show us what you can do!
 
-## ⚖️ Code of Conduct
+### ⚖️ Code of Conduct
 
 Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](/code-of-conduct.md).
 
-## :shipit: GitHub Issues
+### :shipit: GitHub Issues
 
-### 🐛 Bugs
+#### 🐛 Bugs
 
 If you think you have found a bug, please follow these steps:
 
@@ -162,7 +140,7 @@ If you think you have found a bug, please follow these steps:
 - Give it a meaningful title to help others who might be searching for your issue in the future.
 - For questions, reach out to the Cluster API community on the [Kubernetes Slack channel][slack_info].
 
-### 🌟 Tracking New Features
+#### 🌟 Tracking New Features
 
 We also use the issue tracker to track features. If you have an idea for a feature or think that you can help Cluster API Provider Hetzner become even more awesome, then follow these steps:
 
@@ -172,9 +150,14 @@ We also use the issue tracker to track features. If you have an idea for a featu
 - Some of our larger features will require some design. If you would like to include a technical design for your feature, please include it in the issue.
 - Once the new feature is well understood and the design is agreed upon, we can start coding. We would love for you to take part in this process, so we encourage you to take the lead and start coding it yourself. Please open a **WIP** _(work in progress)_ pull request. Happy coding!
 
-## 📃 License
+## 💬 Contact
+
+For more information about Syself, our platform, or any generall information about the Cluster API Provider Hetzner, feel free to reach out to us. Below are some ways to contact our team:
 
-Published under the [Apache](https://github.com/syself/cluster-api-provider-hetzner/blob/main/LICENSE) license.
+- **Email**: Send us questions at contact@syself.com
+- **Website**: Visit [our website](https://syself.com) for more information about Syself
+- **LinkedIn**: Follow us on [LinkedIn](https://www.linkedin.com/company/syself/) for announcements
+- **Newsletter**: Consider subscribing to [our LinkedIn newsletter](https://www.linkedin.com/newsletters/the-syselfer-7223788357485543424/) for regular news about CAPH
 
 <!-- References -->
 
diff --git a/api/v1beta1/hetznerbaremetalhost_types.go b/api/v1beta1/hetznerbaremetalhost_types.go
index 4b87f9d8..47be9682 100644
--- a/api/v1beta1/hetznerbaremetalhost_types.go
+++ b/api/v1beta1/hetznerbaremetalhost_types.go
@@ -152,9 +152,6 @@ const (
 	// StateImageInstalling means we install a new image.
 	StateImageInstalling ProvisioningState = "image-installing"
 
-	// StateProvisioning means we are sending userData to the host and booting the machine.
-	StateProvisioning ProvisioningState = "provisioning"
-
 	// StateEnsureProvisioned means we are ensuring the reboot worked and cloud-init was executed successfully.
 	StateEnsureProvisioned ProvisioningState = "ensure-provisioned"
 
diff --git a/api/v1beta1/hetznerbaremetalmachine_types.go b/api/v1beta1/hetznerbaremetalmachine_types.go
index 2952a96f..42b40760 100644
--- a/api/v1beta1/hetznerbaremetalmachine_types.go
+++ b/api/v1beta1/hetznerbaremetalmachine_types.go
@@ -149,7 +149,7 @@ type InstallImage struct {
 	// Image is the image to be provisioned. It defines the image for baremetal machine.
 	Image Image `json:"image"`
 
-	// PostInstallScript is used for configuring commands that should be executed after installimage.
+	// PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
 	// It is passed along with the installimage command.
 	PostInstallScript string `json:"postInstallScript,omitempty"`
 
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
index 45a71431..5317afa2 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
@@ -441,7 +441,7 @@ spec:
                         type: array
                       postInstallScript:
                         description: |-
-                          PostInstallScript is used for configuring commands that should be executed after installimage.
+                          PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                           It is passed along with the installimage command.
                         type: string
                       swraid:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
index 83402f08..c29e06e4 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
@@ -220,7 +220,7 @@ spec:
                     type: array
                   postInstallScript:
                     description: |-
-                      PostInstallScript is used for configuring commands that should be executed after installimage.
+                      PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                       It is passed along with the installimage command.
                     type: string
                   swraid:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
index c45afa75..9625e4c7 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
@@ -207,7 +207,7 @@ spec:
                             type: array
                           postInstallScript:
                             description: |-
-                              PostInstallScript is used for configuring commands that should be executed after installimage.
+                              PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                               It is passed along with the installimage command.
                             type: string
                           swraid:
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index 92579bc7..0965bc4e 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -66,7 +66,6 @@ type HetznerBareMetalHostReconciler struct {
 // Reconcile implements the reconcilement of HetznerBareMetalHost objects.
 func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) {
 	log := ctrl.LoggerFrom(ctx)
-
 	// Fetch the Hetzner bare metal host instance.
 	bmHost := &infrav1.HetznerBareMetalHost{}
 	err := r.Get(ctx, req.NamespacedName, bmHost)
@@ -77,8 +76,6 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
 		return reconcile.Result{}, err
 	}
 
-	log = log.WithValues("HetznerBareMetalHost", klog.KObj(bmHost))
-
 	// Add a finalizer to newly created objects.
 	if bmHost.DeletionTimestamp.IsZero() && !hostHasFinalizer(bmHost) {
 		bmHost.Finalizers = append(bmHost.Finalizers,
diff --git a/hack/filter-caph-controller-manager-logs.py b/hack/filter-caph-controller-manager-logs.py
index dfa936fa..4e618092 100755
--- a/hack/filter-caph-controller-manager-logs.py
+++ b/hack/filter-caph-controller-manager-logs.py
@@ -29,14 +29,24 @@ rows_to_skip = [
     'Registering a mutating webhook', 'Starting EventSource',
     'Starting Controller',
     '"Starting workers" controller/controller',
+    '"starting manager"',
+    '"Starting metrics server"',
+    '"starting server"',
     '"Reconciling finished"',
     '"Creating cluster scope"',
     '"Starting reconciling cluster"',
     '"Completed function"',
     '"Adding request."',
+    '"Serving metrics server"',
+    '"Starting webhook server"',
+    '"Serving webhook server"',
+    'attempting to acquire leader lease caph-system/hetzner.cluster.x-k8s.io.',
     'Update to resource only changes insignificant fields',
     '"approved csr"',
     '"Registering webhook"',
+    '"Stopping and waiting for',
+    '"All workers finished"',
+    '"Shutdown signal received, waiting for all workers to finish"',
     "'statusCode': 200, 'method': 'GET', 'url': 'https://robot-ws.your-server.de",
 ]
 
diff --git a/pkg/services/baremetal/baremetal/baremetal.go b/pkg/services/baremetal/baremetal/baremetal.go
index 8b835f8a..ba619fa5 100644
--- a/pkg/services/baremetal/baremetal/baremetal.go
+++ b/pkg/services/baremetal/baremetal/baremetal.go
@@ -504,7 +504,7 @@ func (s *Service) skipHost(labelSelector labels.Selector, host infrav1.HetznerBa
 		// Machine should have RAID. Skip machines which have less than two WWNs
 		lenOfWwnSlice := len(host.Spec.RootDeviceHints.Raid.WWN)
 		if lenOfWwnSlice < 2 {
-			mapOfSkipReasons[fmt.Sprintf("machine-should-use-swraid-but-only-%d-RAID-WWN-in-hbmh", lenOfWwnSlice)]++
+			mapOfSkipReasons["machine-should-use-swraid-but-not-enough-RAID-WWNs-in-hbmh"]++
 			return true
 		}
 		return false
diff --git a/pkg/services/baremetal/client/mocks/ssh/Client.go b/pkg/services/baremetal/client/mocks/ssh/Client.go
index db669999..f1590312 100644
--- a/pkg/services/baremetal/client/mocks/ssh/Client.go
+++ b/pkg/services/baremetal/client/mocks/ssh/Client.go
@@ -246,97 +246,6 @@ func (_c *Client_CreateAutoSetup_Call) RunAndReturn(run func(string) sshclient.O
 	return _c
 }
 
-// CreateMetaData provides a mock function with given fields: hostName
-func (_m *Client) CreateMetaData(hostName string) sshclient.Output {
-	ret := _m.Called(hostName)
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateMetaData")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func(string) sshclient.Output); ok {
-		r0 = rf(hostName)
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateMetaData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMetaData'
-type Client_CreateMetaData_Call struct {
-	*mock.Call
-}
-
-// CreateMetaData is a helper method to define mock.On call
-//   - hostName string
-func (_e *Client_Expecter) CreateMetaData(hostName interface{}) *Client_CreateMetaData_Call {
-	return &Client_CreateMetaData_Call{Call: _e.mock.On("CreateMetaData", hostName)}
-}
-
-func (_c *Client_CreateMetaData_Call) Run(run func(hostName string)) *Client_CreateMetaData_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(string))
-	})
-	return _c
-}
-
-func (_c *Client_CreateMetaData_Call) Return(_a0 sshclient.Output) *Client_CreateMetaData_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateMetaData_Call) RunAndReturn(run func(string) sshclient.Output) *Client_CreateMetaData_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
-// CreateNoCloudDirectory provides a mock function with given fields:
-func (_m *Client) CreateNoCloudDirectory() sshclient.Output {
-	ret := _m.Called()
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateNoCloudDirectory")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func() sshclient.Output); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateNoCloudDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateNoCloudDirectory'
-type Client_CreateNoCloudDirectory_Call struct {
-	*mock.Call
-}
-
-// CreateNoCloudDirectory is a helper method to define mock.On call
-func (_e *Client_Expecter) CreateNoCloudDirectory() *Client_CreateNoCloudDirectory_Call {
-	return &Client_CreateNoCloudDirectory_Call{Call: _e.mock.On("CreateNoCloudDirectory")}
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) Run(run func()) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run()
-	})
-	return _c
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) Return(_a0 sshclient.Output) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) RunAndReturn(run func() sshclient.Output) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // CreatePostInstallScript provides a mock function with given fields: data
 func (_m *Client) CreatePostInstallScript(data string) sshclient.Output {
 	ret := _m.Called(data)
@@ -383,52 +292,6 @@ func (_c *Client_CreatePostInstallScript_Call) RunAndReturn(run func(string) ssh
 	return _c
 }
 
-// CreateUserData provides a mock function with given fields: userData
-func (_m *Client) CreateUserData(userData string) sshclient.Output {
-	ret := _m.Called(userData)
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateUserData")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func(string) sshclient.Output); ok {
-		r0 = rf(userData)
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateUserData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUserData'
-type Client_CreateUserData_Call struct {
-	*mock.Call
-}
-
-// CreateUserData is a helper method to define mock.On call
-//   - userData string
-func (_e *Client_Expecter) CreateUserData(userData interface{}) *Client_CreateUserData_Call {
-	return &Client_CreateUserData_Call{Call: _e.mock.On("CreateUserData", userData)}
-}
-
-func (_c *Client_CreateUserData_Call) Run(run func(userData string)) *Client_CreateUserData_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(string))
-	})
-	return _c
-}
-
-func (_c *Client_CreateUserData_Call) Return(_a0 sshclient.Output) *Client_CreateUserData_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateUserData_Call) RunAndReturn(run func(string) sshclient.Output) *Client_CreateUserData_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // DetectLinuxOnAnotherDisk provides a mock function with given fields: sliceOfWwns
 func (_m *Client) DetectLinuxOnAnotherDisk(sliceOfWwns []string) sshclient.Output {
 	ret := _m.Called(sliceOfWwns)
@@ -522,51 +385,6 @@ func (_c *Client_DownloadImage_Call) RunAndReturn(run func(string, string) sshcl
 	return _c
 }
 
-// EnsureCloudInit provides a mock function with given fields:
-func (_m *Client) EnsureCloudInit() sshclient.Output {
-	ret := _m.Called()
-
-	if len(ret) == 0 {
-		panic("no return value specified for EnsureCloudInit")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func() sshclient.Output); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_EnsureCloudInit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnsureCloudInit'
-type Client_EnsureCloudInit_Call struct {
-	*mock.Call
-}
-
-// EnsureCloudInit is a helper method to define mock.On call
-func (_e *Client_Expecter) EnsureCloudInit() *Client_EnsureCloudInit_Call {
-	return &Client_EnsureCloudInit_Call{Call: _e.mock.On("EnsureCloudInit")}
-}
-
-func (_c *Client_EnsureCloudInit_Call) Run(run func()) *Client_EnsureCloudInit_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run()
-	})
-	return _c
-}
-
-func (_c *Client_EnsureCloudInit_Call) Return(_a0 sshclient.Output) *Client_EnsureCloudInit_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_EnsureCloudInit_Call) RunAndReturn(run func() sshclient.Output) *Client_EnsureCloudInit_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // ExecuteInstallImage provides a mock function with given fields: hasPostInstallScript
 func (_m *Client) ExecuteInstallImage(hasPostInstallScript bool) sshclient.Output {
 	ret := _m.Called(hasPostInstallScript)
diff --git a/pkg/services/baremetal/client/ssh/ssh_client.go b/pkg/services/baremetal/client/ssh/ssh_client.go
index c9345e0a..6e76134c 100644
--- a/pkg/services/baremetal/client/ssh/ssh_client.go
+++ b/pkg/services/baremetal/client/ssh/ssh_client.go
@@ -176,6 +176,30 @@ type Output struct {
 	Err    error
 }
 
+func (o Output) String() string {
+	s := make([]string, 0, 3)
+	stdout := strings.TrimSpace(o.StdOut)
+	if stdout != "" {
+		s = append(s, stdout)
+	}
+	stderr := strings.TrimSpace(o.StdErr)
+	if stderr != "" {
+		if len(s) > 0 {
+			stderr = "Stderr: " + stderr
+		}
+		s = append(s, stderr)
+	}
+	if o.Err != nil {
+		e := o.Err.Error()
+		e = strings.TrimSpace(e)
+		if len(s) > 0 {
+			e = "Err: " + e
+		}
+		s = append(s, e)
+	}
+	return strings.Join(s, ". ")
+}
+
 // Client is the interface defining all functions necessary to talk to a bare metal server via SSH.
 type Client interface {
 	GetHostName() Output
@@ -196,10 +220,6 @@ type Client interface {
 	CreatePostInstallScript(data string) Output
 	ExecuteInstallImage(hasPostInstallScript bool) Output
 	Reboot() Output
-	EnsureCloudInit() Output
-	CreateNoCloudDirectory() Output
-	CreateMetaData(hostName string) Output
-	CreateUserData(userData string) Output
 	CloudInitStatus() Output
 	CheckCloudInitLogsForSigTerm() Output
 	CleanCloudInitLogs() Output
@@ -326,7 +346,7 @@ func (c *sshClient) GetHardwareDetailsCPUCores() Output {
 
 // GetHardwareDetailsDebug implements the GetHardwareDetailsDebug method of the SSHClient interface.
 func (c *sshClient) GetHardwareDetailsDebug() Output {
-	return c.runSSH(`ip a; echo ==========----------==========; 
+	return c.runSSH(`ip a; echo ==========----------==========;
 	ethtool "*"; echo ==========----------==========;
 	lspci; echo ==========----------==========;
 	`)
@@ -362,7 +382,7 @@ EOF_VIA_SSH`, data))
 	if out.Err != nil || out.StdErr != "" {
 		return out
 	}
-	return c.runSSH(`chmod +x /root/post-install.sh . `)
+	return c.runSSH(`chmod +x /root/post-install.sh`)
 }
 
 // GetRunningInstallImageProcesses returns the running installimage processes. Output.StdOut is empty if no processes are running.
@@ -432,30 +452,6 @@ func (c *sshClient) Reboot() Output {
 	return out
 }
 
-// EnsureCloudInit implements the EnsureCloudInit method of the SSHClient interface.
-func (c *sshClient) EnsureCloudInit() Output {
-	return c.runSSH(`command -v cloud-init`)
-}
-
-// CreateNoCloudDirectory implements the CreateNoCloudDirectory method of the SSHClient interface.
-func (c *sshClient) CreateNoCloudDirectory() Output {
-	return c.runSSH(`mkdir -p /var/lib/cloud/seed/nocloud-net`)
-}
-
-// CreateMetaData implements the CreateMetaData method of the SSHClient interface.
-func (c *sshClient) CreateMetaData(hostName string) Output {
-	return c.runSSH(fmt.Sprintf(`cat << 'EOF_VIA_SSH' > /var/lib/cloud/seed/nocloud-net/meta-data
-local-hostname: %s
-EOF_VIA_SSH`, hostName))
-}
-
-// CreateUserData implements the CreateUserData method of the SSHClient interface.
-func (c *sshClient) CreateUserData(userData string) Output {
-	return c.runSSH(fmt.Sprintf(`cat << 'EOF_VIA_SSH' > /var/lib/cloud/seed/nocloud-net/user-data
-%s
-EOF_VIA_SSH`, userData))
-}
-
 // CloudInitStatus implements the CloudInitStatus method of the SSHClient interface.
 func (c *sshClient) CloudInitStatus() Output {
 	out := c.runSSH("cloud-init status")
diff --git a/pkg/services/baremetal/host/host.go b/pkg/services/baremetal/host/host.go
index d7e9c9cc..9f096fd0 100644
--- a/pkg/services/baremetal/host/host.go
+++ b/pkg/services/baremetal/host/host.go
@@ -64,6 +64,10 @@ const (
 	errMsgInvalidSSHStdOut             = "invalid output in stdOut: %w"
 	errMsgFailedHandlingIncompleteBoot = "failed to handle incomplete boot: %w"
 	rebootServerStr                    = "RebootBMServer"
+
+	// PostInstallScriptFinished is a marker in the output of installimage. If it is not present,
+	// then install-image failed.
+	PostInstallScriptFinished = "POST_INSTALL_SCRIPT_FINISHED"
 )
 
 var (
@@ -130,7 +134,7 @@ func (s *Service) Reconcile(ctx context.Context) (result reconcile.Result, err e
 	}()
 
 	// reconcile state
-	actResult := hostStateMachine.ReconcileState()
+	actResult := hostStateMachine.ReconcileState(ctx)
 
 	result, err = actResult.Result()
 	if err != nil {
@@ -162,7 +166,9 @@ func SaveHostAndReturn(ctx context.Context, cl client.Client, host *infrav1.Hetz
 	return res, nil
 }
 
-func (s *Service) actionPreparing() actionResult {
+// previous: None
+// next: Registering
+func (s *Service) actionPreparing(_ context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StatePreparing)
 
 	server, err := s.scope.RobotClient.GetBMServer(s.scope.HetznerBareMetalHost.Spec.ServerID)
@@ -238,14 +244,14 @@ func (s *Service) actionPreparing() actionResult {
 	if trimLineBreak(out.StdOut) != "" {
 		// we managed access with ssh - we can do an ssh reboot
 		if err := handleSSHError(sshClient.Reboot()); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot server via ssh: %w", err)}
+			return actionError{err: fmt.Errorf("failed to reboot server via ssh (actionPreparing): %w", err)}
 		}
 		msg := "Rebooting into rescue mode."
 		createSSHRebootEvent(s.scope.HetznerBareMetalHost, msg)
 		// we immediately set an error message in the host status to track the reboot we just performed
 		s.scope.HetznerBareMetalHost.SetError(infrav1.ErrorTypeSSHRebootTriggered, fmt.Sprintf("Phase %s, reboot via ssh: %s",
 			s.scope.HetznerBareMetalHost.Spec.Status.ProvisioningState, msg))
-		return actionComplete{}
+		return actionComplete{} // next: Registering
 	}
 
 	// Check if software reboot is available. If it is not, choose hardware reboot.
@@ -259,7 +265,7 @@ func (s *Service) actionPreparing() actionResult {
 	msg := createRebootEvent(s.scope.HetznerBareMetalHost, rebootType, "Reboot into rescue system.")
 	// we immediately set an error message in the host status to track the reboot we just performed
 	s.scope.HetznerBareMetalHost.SetError(errorType, msg)
-	return actionComplete{}
+	return actionComplete{} // next: Registering
 }
 
 func (s *Service) enforceRescueMode() error {
@@ -564,7 +570,9 @@ func (s *Service) ensureRescueMode() error {
 	return nil
 }
 
-func (s *Service) actionRegistering() actionResult {
+// previous: Preparing
+// next: ImageInstalling
+func (s *Service) actionRegistering(_ context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateRegistering)
 
 	creds := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef)
@@ -687,7 +695,7 @@ func (s *Service) actionRegistering() actionResult {
 
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.RootDeviceHintsValidatedCondition)
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: ImageInstalling
 }
 
 func validateRootDeviceWwnsAreSubsetOfExistingWwns(rootDeviceHints *infrav1.RootDeviceHints, storageDevices []infrav1.Storage) error {
@@ -722,8 +730,8 @@ func getHardwareDetails(sshClient sshclient.Client) (infrav1.HardwareDetails, er
 	}
 
 	// remove names of storage devices because they might change
-	for _, device := range storage {
-		device.Name = ""
+	for i := range storage {
+		storage[i].Name = ""
 	}
 
 	cpu, err := obtainHardwareDetailsCPU(sshClient)
@@ -1046,7 +1054,9 @@ func validateStdOut(stdOut string) (string, error) {
 	return stdOut, nil
 }
 
-func (s *Service) actionImageInstalling() actionResult {
+// previous: Registering
+// next: EnsureProvisioned
+func (s *Service) actionImageInstalling(ctx context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateImageInstalling)
 
 	creds := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef)
@@ -1112,11 +1122,10 @@ func (s *Service) actionImageInstalling() actionResult {
 		return actionError{err: fmt.Errorf("failed to get running installimage processes: %q %q %w", out.StdOut, out.StdErr, out.Err)}
 	}
 	if out.StdOut != "" {
-		record.Warnf(s.scope.HetznerBareMetalHost, "InstallImageAlreadyRunning",
-			"installimage is already running:\n%s", out.StdOut)
-		return actionContinue{
-			delay: 10 * time.Second,
-		}
+		// This can happen if the controller was restarted.
+		record.Warn(s.scope.HetznerBareMetalHost, "InstallImageAlreadyRunning",
+			"installimage is already running.")
+		return actionError{err: fmt.Errorf("installimage is already running")}
 	}
 
 	record.Event(s.scope.HetznerBareMetalHost, "InstallImagePreflightCheckSuccessful", "Rescue system reachable, disks look good.")
@@ -1140,10 +1149,38 @@ func (s *Service) actionImageInstalling() actionResult {
 	// create post install script
 	postInstallScript := s.scope.HetznerBareMetalHost.Spec.Status.InstallImage.PostInstallScript
 
-	if postInstallScript != "" {
-		if err := handleSSHError(sshClient.CreatePostInstallScript(postInstallScript)); err != nil {
-			return actionError{err: fmt.Errorf("failed to create post install script %s: %w", postInstallScript, err)}
-		}
+	if !strings.HasPrefix(postInstallScript, "#!/bin/bash") {
+		postInstallScript = fmt.Sprintf("#!/bin/bash\n%s", postInstallScript)
+	}
+
+	cloudInitData, err := s.scope.GetRawBootstrapData(ctx)
+	if err != nil {
+		return actionError{err: fmt.Errorf("failed to get user data: %w", err)}
+	}
+
+	postInstallScript = fmt.Sprintf(`%s
+
+# install cloud-init data
+
+trap 'echo "ERROR: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0")"; exit 3' ERR
+set -Eeuo pipefail
+
+mkdir -p /var/lib/cloud/seed/nocloud-net
+
+cat << 'EOF_POST_INSTALL_SCRIPT' > /var/lib/cloud/seed/nocloud-net/meta-data
+local-hostname: %s
+EOF_POST_INSTALL_SCRIPT
+
+cat << 'EOF_POST_INSTALL_SCRIPT' > /var/lib/cloud/seed/nocloud-net/user-data
+%s
+EOF_POST_INSTALL_SCRIPT
+
+echo %q
+# end of install cloud-init data
+`, postInstallScript, s.scope.Hostname(), cloudInitData, PostInstallScriptFinished)
+
+	if err := handleSSHError(sshClient.CreatePostInstallScript(postInstallScript)); err != nil {
+		return actionError{err: fmt.Errorf("failed to create post install script %s: %w", postInstallScript, err)}
 	}
 
 	record.Event(s.scope.HetznerBareMetalHost, "InstallingMachineImageStarted",
@@ -1160,10 +1197,14 @@ func (s *Service) actionImageInstalling() actionResult {
 	// Execute install image
 	out = sshClient.ExecuteInstallImage(postInstallScript != "")
 	if out.Err != nil {
-		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageFailed", out.StdOut)
+		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageFailed", out.String())
 		return actionError{err: fmt.Errorf("failed to execute installimage: %w", out.Err)}
 	}
-
+	if !strings.Contains(out.StdOut, PostInstallScriptFinished) {
+		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageDidNotFinish", out.StdOut)
+		return actionError{err: fmt.Errorf("did not find marker %q in stdout. Failed to execute installimage",
+			PostInstallScriptFinished)}
+	}
 	record.Eventf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageSucceeded", out.StdOut)
 	s.scope.Logger.Info("ExecuteInstallImageSucceeded", "stdout", out.StdOut, "stderr", out.StdErr)
 
@@ -1174,17 +1215,19 @@ func (s *Service) actionImageInstalling() actionResult {
 		return actionError{err: fmt.Errorf("failed to update name of host in robot API: %w", err)}
 	}
 
-	if err := handleSSHError(sshClient.Reboot()); err != nil {
+	out = sshClient.Reboot()
+	if err := handleSSHError(out); err != nil {
+		err = fmt.Errorf("failed to reboot server (after install-image): %w", err)
 		record.Warn(s.scope.HetznerBareMetalHost, "RebootFailed", err.Error())
-		return actionError{err: fmt.Errorf("failed to reboot server: %w", err)}
+		return actionError{err: err}
 	}
-	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image was installed")
+	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image and cloud-init data got installed")
 
 	s.scope.Logger.Info("RebootAfterInstallimageSucceeded", "stdout", out.StdOut, "stderr", out.StdErr)
 
 	// clear potential errors - all done
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: ensure-provisioned
 }
 
 func (s *Service) createAutoSetupInput(sshClient sshclient.Client) (autoSetupInput, actionResult) {
@@ -1255,129 +1298,6 @@ func getDeviceNames(wwn []string, storageDevices []infrav1.Storage) []string {
 	return deviceNames
 }
 
-func (s *Service) actionProvisioning() actionResult {
-	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateProvisioning)
-
-	host := s.scope.HetznerBareMetalHost
-
-	portAfterInstallImage := host.Spec.Status.SSHSpec.PortAfterInstallImage
-	privateKey := sshclient.CredentialsFromSecret(s.scope.OSSSHSecret, host.Spec.Status.SSHSpec.SecretRef).PrivateKey
-	sshClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
-		PrivateKey: privateKey,
-		Port:       portAfterInstallImage,
-		IP:         host.Spec.Status.GetIPAddress(),
-	})
-
-	// check hostname with sshClient
-	wantHostName := s.scope.Hostname()
-
-	out := sshClient.GetHostName()
-
-	if out.Err != nil {
-		msg := fmt.Sprintf("ssh to port %d failed: %s (%s): %v", portAfterInstallImage, out.StdOut, out.StdErr, out.Err)
-		conditions.MarkFalse(host,
-			infrav1.SSHAfterInstallImageSucceededCondition,
-			infrav1.SSHAfterInstallImageFailedReason,
-			clusterv1.ConditionSeverityWarning, msg)
-
-		if time.Now().After(conditions.GetLastTransitionTime(
-			s.scope.HetznerBareMetalHost,
-			infrav1.SSHAfterInstallImageSucceededCondition).Add(7 * time.Minute)) {
-			// We waited some minutes. Still no connection. There seems to something wrong.
-			record.Warn(host, infrav1.SSHAfterInstallImageFailedReason, msg)
-		}
-	} else {
-		conditions.MarkTrue(s.scope.HetznerBareMetalHost,
-			infrav1.SSHAfterInstallImageSucceededCondition)
-	}
-
-	if trimLineBreak(out.StdOut) != wantHostName {
-		// Give the reboot some time until it takes effect. Otherwise the ssh connection gets done too fast,
-		// and it will connect to the machine before it gets rebooted.
-		if s.hasJustRebooted() {
-			s.scope.Logger.Info("hasJustRebooted is true", "LastUpdated", host.Spec.Status.LastUpdated)
-			return actionContinue{delay: 2 * time.Second}
-		}
-
-		privateKeyRescue := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef).PrivateKey
-		rescueSSHClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
-			PrivateKey: privateKeyRescue,
-			Port:       rescuePort,
-			IP:         host.Spec.Status.GetIPAddress(),
-		})
-
-		isSSHTimeoutError, isSSHConnectionRefusedError, err := analyzeSSHOutputInstallImage(out, rescueSSHClient, portAfterInstallImage)
-		if err != nil {
-			if errors.Is(err, errUnexpectedHostName) {
-				// One possible reason: The machine gets used by a second wl-cluster
-				record.Warn(host, "UnexpectedHostName",
-					fmt.Sprintf("Provisioning: wanted %q. %s", wantHostName, err.Error()))
-			}
-			return actionError{err: fmt.Errorf("failed to handle incomplete boot - installImage: %w", err)}
-		}
-		failed, err := s.handleIncompleteBoot(false, isSSHTimeoutError, isSSHConnectionRefusedError)
-		if failed {
-			return s.recordActionFailure(infrav1.ProvisioningError, err.Error())
-		}
-		if err != nil {
-			return actionError{err: fmt.Errorf(errMsgFailedHandlingIncompleteBoot, err)}
-		}
-		return actionContinue{delay: 10 * time.Second}
-	}
-
-	// we are in correct boot and can start provisioning
-	if failedAction := s.provision(sshClient); failedAction != nil {
-		return failedAction
-	}
-
-	host.ClearError()
-	return actionComplete{}
-}
-
-func (s *Service) provision(sshClient sshclient.Client) actionResult {
-	{
-		out := sshClient.EnsureCloudInit()
-		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to ensure cloud init: %w", err)}
-		}
-
-		if trimLineBreak(out.StdOut) == "" {
-			msg := "cloud init not installed"
-			conditions.MarkFalse(
-				s.scope.HetznerBareMetalHost,
-				infrav1.ProvisionSucceededCondition,
-				infrav1.CloudInitNotInstalledReason,
-				clusterv1.ConditionSeverityError,
-				msg,
-			)
-			return s.recordActionFailure(infrav1.ProvisioningError, msg)
-		}
-	}
-
-	if err := handleSSHError(sshClient.CreateNoCloudDirectory()); err != nil {
-		return actionError{err: fmt.Errorf("failed to create no cloud directory: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.CreateMetaData(s.scope.Hostname())); err != nil {
-		return actionError{err: fmt.Errorf("failed to create meta data: %w", err)}
-	}
-
-	userData, err := s.scope.GetRawBootstrapData(context.TODO())
-	if err != nil {
-		return actionError{err: fmt.Errorf("failed to get user data: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.CreateUserData(string(userData))); err != nil {
-		return actionError{err: fmt.Errorf("failed to create user data: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.Reboot()); err != nil {
-		return actionError{err: fmt.Errorf("failed to reboot: %w", err)}
-	}
-	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "UserData of cloud-init was created")
-	return nil
-}
-
 func analyzeSSHOutputInstallImage(out sshclient.Output, sshClient sshclient.Client, port int) (isTimeout, isConnectionRefused bool, reterr error) {
 	// check err
 	if out.Err != nil {
@@ -1441,7 +1361,7 @@ func verifyConnectionRefused(sshClient sshclient.Client, port int) bool {
 	return true
 }
 
-func (s *Service) actionEnsureProvisioned() (ar actionResult) {
+func (s *Service) actionEnsureProvisioned(_ context.Context) (ar actionResult) {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateEnsureProvisioned)
 	sshClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
 		PrivateKey: sshclient.CredentialsFromSecret(s.scope.OSSSHSecret, s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.SecretRef).PrivateKey,
@@ -1482,9 +1402,11 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	wantHostName := s.scope.Hostname()
 
 	out := sshClient.GetHostName()
-	if trimLineBreak(out.StdOut) != wantHostName {
+	hostname := trimLineBreak(out.StdOut)
+	if hostname != wantHostName {
 		// give the reboot some time until it takes effect
 		if s.hasJustRebooted() {
+			s.scope.Logger.Info("ensureProvisioned: hasJustRebooted. Retrying...", "hostname", hostname)
 			return actionContinue{delay: 2 * time.Second}
 		}
 
@@ -1500,6 +1422,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 		// A connection failed error could mean that cloud init is still running (if cloudInit introduces a new port)
 		if isSSHConnectionRefusedError {
 			if actionRes := s.handleConnectionRefused(); actionRes != nil {
+				s.scope.Logger.Info("ensureProvisioned: ConnectionRefused", "actionResult", actionRes)
 				return actionRes
 			}
 		}
@@ -1515,8 +1438,9 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	}
 
 	// Check the status of cloud init
-	actResult, _ := s.checkCloudInitStatus(sshClient)
+	actResult, msg, _ := s.checkCloudInitStatus(sshClient)
 	if _, complete := actResult.(actionComplete); !complete {
+		record.Event(s.scope.HetznerBareMetalHost, "CloudInitStillRunning", msg)
 		return actResult
 	}
 
@@ -1526,6 +1450,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	if s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterInstallImage == s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterCloudInit {
 		actResult = s.handleCloudInitNotStarted()
 		if _, complete := actResult.(actionComplete); !complete {
+			s.scope.Logger.Info("ensureProvisioned: handleCloudInitNotStarted", "actResult", actResult)
 			return actResult
 		}
 	}
@@ -1533,7 +1458,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	record.Event(s.scope.HetznerBareMetalHost, "ServerProvisioned", "server successfully provisioned")
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: EnsureProvisioned
 }
 
 // handleConnectionRefused checks cloud init status via ssh to the old ssh port if the new ssh port
@@ -1548,7 +1473,7 @@ func (s *Service) handleConnectionRefused() actionResult {
 		Port:       s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterInstallImage,
 		IP:         s.scope.HetznerBareMetalHost.Spec.Status.GetIPAddress(),
 	})
-	actResult, err := s.checkCloudInitStatus(oldSSHClient)
+	actResult, _, err := s.checkCloudInitStatus(oldSSHClient)
 	// If this ssh client also gives an error, then we go back to analyzing the error of the first ssh call
 	// This happens in the statement below this one.
 	if err == nil {
@@ -1569,40 +1494,42 @@ func (s *Service) handleConnectionRefused() actionResult {
 	return nil
 }
 
-func (s *Service) checkCloudInitStatus(sshClient sshclient.Client) (actionResult, error) {
+func (s *Service) checkCloudInitStatus(sshClient sshclient.Client) (actionResult, string, error) {
 	out := sshClient.CloudInitStatus()
 	// This error is interesting for further logic and might happen because of the fact that the sshClient has the wrong port
 	if out.Err != nil {
-		return actionError{err: fmt.Errorf("failed to get cloud init status: %w", out.Err)}, out.Err
+		return actionError{err: fmt.Errorf("failed to get cloud init status: %w", out.Err)}, "", out.Err
 	}
 
 	stdOut := trimLineBreak(out.StdOut)
 	switch {
 	case strings.Contains(stdOut, "status: running"):
 		// Cloud init is still running
-		return actionContinue{delay: 5 * time.Second}, nil
+		return actionContinue{delay: 5 * time.Second}, "cloud-init still running", nil
 	case strings.Contains(stdOut, "status: disabled"):
 		// Reboot needs to be triggered again - did not start yet
 		out = sshClient.Reboot()
+		msg := "cloud-init-status was 'disabled'"
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot: %w", err)}, nil
+			return actionError{err: fmt.Errorf("failed to reboot (%s): %w", msg, err)}, "", nil
 		}
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, msg)
 		s.scope.HetznerBareMetalHost.SetError(infrav1.ErrorTypeSSHRebootTriggered, "ssh reboot just triggered")
-		record.Event(s.scope.HetznerBareMetalHost, "SSHRebootAfterCloudInitStatusDisabled", "cloud-init status was 'disabled'")
-		return actionContinue{delay: 5 * time.Second}, nil
+		record.Warn(s.scope.HetznerBareMetalHost, "SSHRebootAfterCloudInitStatusDisabled", msg)
+		return actionContinue{delay: 5 * time.Second}, "cloud-init was disabled. Triggered a reboot again", nil
 	case strings.Contains(stdOut, "status: done"):
-
 		s.scope.HetznerBareMetalHost.ClearError()
+		return actionComplete{}, "cloud-init is done", nil
 	case strings.Contains(stdOut, "status: error"):
 		record.Warn(s.scope.HetznerBareMetalHost, "CloudInitFailed", "cloud init returned status error")
-		return s.recordActionFailure(infrav1.FatalError, "cloud init returned status error"), nil
+		return s.recordActionFailure(infrav1.FatalError, "cloud init returned status error"), "", nil
 	default:
 		// Errors are handled after stdOut in this case, as status: error returns an exited with status 1 error
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to get cloud init status: %w", err)}, nil
+			return actionError{err: fmt.Errorf("failed to get cloud init status: %w", err)}, "", nil
 		}
+		return actionContinue{delay: 5 * time.Second}, fmt.Sprintf("cloud-init unknown output: %s. %s", out.StdOut, out.StdErr), nil
 	}
-	return actionComplete{}, nil
 }
 
 func (s *Service) handleCloudInitNotStarted() actionResult {
@@ -1629,8 +1556,9 @@ func (s *Service) handleCloudInitNotStarted() actionResult {
 		}
 		out = oldSSHClient.Reboot()
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot: %w", err)}
+			return actionError{err: fmt.Errorf("failed to reboot (handleCloudInitNotStarted): %w", err)}
 		}
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image and cloud-init data got installed")
 		record.Eventf(s.scope.HetznerBareMetalHost,
 			"SSHRebootAfterCloudInitSigTermFound", "rebooted via ssh after cloud init logs contained sigterm: %s", trimLineBreak(out.StdOut))
 		return actionContinue{delay: 10 * time.Second}
@@ -1675,7 +1603,9 @@ func analyzeSSHOutputProvisioned(out sshclient.Output) (isTimeout, isConnectionR
 	return false, false, fmt.Errorf("%w: %s", errUnexpectedHostName, trimLineBreak(out.StdOut))
 }
 
-func (s *Service) actionProvisioned() actionResult {
+// previous: EnsureProvisioned
+// next: Stays in Provisioned (final state)
+func (s *Service) actionProvisioned(_ context.Context) actionResult {
 	// set host to provisioned
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 
@@ -1730,15 +1660,16 @@ func (s *Service) actionProvisioned() actionResult {
 			return actionError{err: err}
 		}
 
-		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "reboot annotation was set")
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "Rebooting because annotation was set")
 		s.scope.HetznerBareMetalHost.Spec.Status.Rebooted = true
 		return actionContinue{delay: 10 * time.Second}
 	}
 
-	return actionComplete{}
+	return actionComplete{} // Stays in Provisioned (final state)
 }
 
-func (s *Service) actionDeprovisioning() actionResult {
+// next: None
+func (s *Service) actionDeprovisioning(_ context.Context) actionResult {
 	// Update name in robot API
 	if _, err := s.scope.RobotClient.SetBMServerName(
 		s.scope.HetznerBareMetalHost.Spec.ServerID,
@@ -1772,11 +1703,10 @@ func (s *Service) actionDeprovisioning() actionResult {
 		s.scope.HetznerBareMetalHost.ClearError()
 		conditions.Delete(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 	}
-
-	return actionComplete{}
+	return actionComplete{} // next: None
 }
 
-func (s *Service) actionDeleting() actionResult {
+func (s *Service) actionDeleting(_ context.Context) actionResult {
 	s.scope.HetznerBareMetalHost.Finalizers = utils.FilterStringFromList(s.scope.HetznerBareMetalHost.Finalizers, infrav1.BareMetalHostFinalizer)
 	return deleteComplete{}
 }
diff --git a/pkg/services/baremetal/host/state_machine.go b/pkg/services/baremetal/host/state_machine.go
index a6086af0..cc336a16 100644
--- a/pkg/services/baremetal/host/state_machine.go
+++ b/pkg/services/baremetal/host/state_machine.go
@@ -17,6 +17,7 @@ limitations under the License.
 package host
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/go-logr/logr"
@@ -51,14 +52,13 @@ func newHostStateMachine(host *infrav1.HetznerBareMetalHost, reconciler *Service
 	return &r
 }
 
-type stateHandler func() actionResult
+type stateHandler func(ctx context.Context) actionResult
 
 func (hsm *hostStateMachine) handlers() map[infrav1.ProvisioningState]stateHandler {
 	return map[infrav1.ProvisioningState]stateHandler{
 		infrav1.StatePreparing:         hsm.handlePreparing,
 		infrav1.StateRegistering:       hsm.handleRegistering,
 		infrav1.StateImageInstalling:   hsm.handleImageInstalling,
-		infrav1.StateProvisioning:      hsm.handleProvisioning,
 		infrav1.StateEnsureProvisioned: hsm.handleEnsureProvisioned,
 		infrav1.StateProvisioned:       hsm.handleProvisioned,
 		infrav1.StateDeprovisioning:    hsm.handleDeprovisioning,
@@ -66,7 +66,7 @@ func (hsm *hostStateMachine) handlers() map[infrav1.ProvisioningState]stateHandl
 	}
 }
 
-func (hsm *hostStateMachine) ReconcileState() (actionRes actionResult) {
+func (hsm *hostStateMachine) ReconcileState(ctx context.Context) (actionRes actionResult) {
 	initialState := hsm.host.Spec.Status.ProvisioningState
 	defer func() {
 		if hsm.nextState != initialState {
@@ -87,8 +87,17 @@ func (hsm *hostStateMachine) ReconcileState() (actionRes actionResult) {
 	// Assume credentials are ready for now. This can be changed while the state is handled.
 	conditions.MarkTrue(hsm.host, infrav1.CredentialsAvailableCondition)
 
+	// This state was removed. We have to handle the edge-case where
+	// the controller got updated and a machine
+	// is in this state. Installing the image again should solve that.
+	if initialState == "provisioning" {
+		hsm.log.Info("edge-case was hit: New code meets machine in removed state 'provisioning'. Re-setting",
+			"new-state", infrav1.StateImageInstalling)
+		initialState = infrav1.StateImageInstalling
+	}
+
 	if stateHandler, found := hsm.handlers()[initialState]; found {
-		return stateHandler()
+		return stateHandler(ctx)
 	}
 
 	return actionError{fmt.Errorf("%w: state %q", errNoHandlerFound, initialState)}
@@ -103,7 +112,7 @@ func (hsm *hostStateMachine) checkInitiateDelete() bool {
 	switch hsm.nextState {
 	default:
 		hsm.nextState = infrav1.StateDeleting
-	case infrav1.StateRegistering, infrav1.StateImageInstalling, infrav1.StateProvisioning,
+	case infrav1.StateRegistering, infrav1.StateImageInstalling,
 		infrav1.StateEnsureProvisioned, infrav1.StateProvisioned:
 		hsm.nextState = infrav1.StateDeprovisioning
 	case infrav1.StateDeprovisioning:
@@ -149,7 +158,7 @@ func (hsm *hostStateMachine) updateOSSSHStatusAndValidateKey(osSSHSecret *corev1
 	if !hsm.host.Spec.Status.SSHStatus.CurrentOS.Match(*osSSHSecret) {
 		// Take action depending on state
 		switch hsm.nextState {
-		case infrav1.StateProvisioning, infrav1.StateEnsureProvisioned:
+		case infrav1.StateEnsureProvisioned:
 			// Go back to StateImageInstalling as we need to provision again
 			hsm.nextState = infrav1.StateImageInstalling
 		case infrav1.StateProvisioned:
@@ -214,7 +223,7 @@ func validateSSHKey(sshSecret *corev1.Secret, secretRef infrav1.SSHSecretRef) er
 	return nil
 }
 
-func (hsm *hostStateMachine) handlePreparing() actionResult {
+func (hsm *hostStateMachine) handlePreparing(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
@@ -222,75 +231,72 @@ func (hsm *hostStateMachine) handlePreparing() actionResult {
 
 	record.Eventf(hsm.host, "PreparingForProvisioning", "ServerID %d %s", hsm.host.Spec.ServerID, hsm.host.Spec.Description)
 
-	actResult := hsm.reconciler.actionPreparing()
+	actResult := hsm.reconciler.actionPreparing(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateRegistering
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleRegistering() actionResult {
+func (hsm *hostStateMachine) handleRegistering(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionRegistering()
+	actResult := hsm.reconciler.actionRegistering(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateImageInstalling
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleImageInstalling() actionResult {
-	if hsm.provisioningCancelled() {
-		hsm.nextState = infrav1.StateDeprovisioning
-		return actionComplete{}
-	}
-
-	actResult := hsm.reconciler.actionImageInstalling()
-	if _, ok := actResult.(actionComplete); ok {
-		hsm.nextState = infrav1.StateProvisioning
-	}
-	return actResult
-}
-
-func (hsm *hostStateMachine) handleProvisioning() actionResult {
+func (hsm *hostStateMachine) handleImageInstalling(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionProvisioning()
-	if _, ok := actResult.(actionComplete); ok {
+	actResult := hsm.reconciler.actionImageInstalling(ctx)
+	switch actResult.(type) {
+	case actionComplete:
 		hsm.nextState = infrav1.StateEnsureProvisioned
+	case actionError:
+		// re-enable rescue system. If installimage failed, then it is likely, that
+		// the next run (without reboot) fails with this error:
+		// ERROR unmounting device(s):
+		// umount: /: target is busy.
+		// Cannot continue, device(s) seem to be in use.
+		// Please unmount used devices manually or reboot the rescuesystem and retry.
+		hsm.nextState = infrav1.StatePreparing
 	}
+
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleEnsureProvisioned() actionResult {
+func (hsm *hostStateMachine) handleEnsureProvisioned(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionEnsureProvisioned()
+	actResult := hsm.reconciler.actionEnsureProvisioned(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateProvisioned
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleProvisioned() actionResult {
+func (hsm *hostStateMachine) handleProvisioned(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
-	return hsm.reconciler.actionProvisioned()
+	return hsm.reconciler.actionProvisioned(ctx)
 }
 
-func (hsm *hostStateMachine) handleDeprovisioning() actionResult {
-	actResult := hsm.reconciler.actionDeprovisioning()
+func (hsm *hostStateMachine) handleDeprovisioning(ctx context.Context) actionResult {
+	actResult := hsm.reconciler.actionDeprovisioning(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateNone
 		return actionComplete{}
@@ -298,8 +304,8 @@ func (hsm *hostStateMachine) handleDeprovisioning() actionResult {
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleDeleting() actionResult {
-	return hsm.reconciler.actionDeleting()
+func (hsm *hostStateMachine) handleDeleting(ctx context.Context) actionResult {
+	return hsm.reconciler.actionDeleting(ctx)
 }
 
 func (hsm *hostStateMachine) provisioningCancelled() bool {


Test patch:
diff --git a/controllers/hcloudmachine_controller_test.go b/controllers/hcloudmachine_controller_test.go
index 6eed7543..37bef3a8 100644
--- a/controllers/hcloudmachine_controller_test.go
+++ b/controllers/hcloudmachine_controller_test.go
@@ -127,9 +127,11 @@ func TestIgnoreInsignificantHCloudMachineStatusUpdates(t *testing.T) {
 		})
 	}
 }
+
 func stPtr(h hcloud.ServerStatus) *hcloud.ServerStatus {
 	return &h
 }
+
 func TestIgnoreInsignificantMachineStatusUpdates(t *testing.T) {
 	logger := klog.Background()
 	predicate := IgnoreInsignificantMachineStatusUpdates(logger)
@@ -361,7 +363,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				}, timeout).Should(BeTrue())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 1", func() {
 				By("checking that no servers exist")
 
 				Eventually(func() bool {
@@ -518,7 +520,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 2", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
@@ -546,7 +548,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 3", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
@@ -594,7 +596,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 4", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
diff --git a/controllers/hetznerbaremetalhost_controller_test.go b/controllers/hetznerbaremetalhost_controller_test.go
index fc244c75..d8d83230 100644
--- a/controllers/hetznerbaremetalhost_controller_test.go
+++ b/controllers/hetznerbaremetalhost_controller_test.go
@@ -36,6 +36,7 @@ import (
 	robotmock "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/mocks/robot"
 	sshmock "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/mocks/ssh"
 	sshclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/ssh"
+	hostpkg "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/host"
 	"github.com/syself/cluster-api-provider-hetzner/pkg/utils"
 	"github.com/syself/cluster-api-provider-hetzner/test/helpers"
 )
@@ -162,10 +163,6 @@ var _ = Describe("HetznerBareMetalHostReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClientAfterInstallImage.On("Reboot").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClientAfterInstallImage.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClientAfterInstallImage.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClientAfterInstallImage.On("ResetKubeadm").Return(sshclient.Output{})
@@ -895,7 +892,7 @@ name="eth0" model="Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express
 	sshClient.On("CreateAutoSetup", mock.Anything).Return(sshclient.Output{})
 	sshClient.On("UntarTGZ").Return(sshclient.Output{})
 	sshClient.On("CreatePostInstallScript", mock.Anything).Return(sshclient.Output{})
-	sshClient.On("ExecuteInstallImage", mock.Anything).Return(sshclient.Output{})
+	sshClient.On("ExecuteInstallImage", mock.Anything).Return(sshclient.Output{StdOut: hostpkg.PostInstallScriptFinished})
 	sshClient.On("Reboot").Return(sshclient.Output{})
 	sshClient.On("GetCloudInitOutput").Return(sshclient.Output{StdOut: "dummy content of /var/log/cloud-init-output.log"})
 	sshClient.On("DetectLinuxOnAnotherDisk", mock.Anything).Return(sshclient.Output{})
diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go
index 00af20fc..b3769b6a 100644
--- a/controllers/hetznerbaremetalmachine_controller_test.go
+++ b/controllers/hetznerbaremetalmachine_controller_test.go
@@ -148,10 +148,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClient.On("Reboot").Return(sshclient.Output{})
-		osSSHClient.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClient.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClient.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClient.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClient.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClient.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClient.On("ResetKubeadm").Return(sshclient.Output{})
diff --git a/controllers/hetznerbaremetalremediation_controller_test.go b/controllers/hetznerbaremetalremediation_controller_test.go
index e77bf665..d7599c0f 100644
--- a/controllers/hetznerbaremetalremediation_controller_test.go
+++ b/controllers/hetznerbaremetalremediation_controller_test.go
@@ -207,10 +207,6 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClientAfterInstallImage.On("Reboot").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClientAfterInstallImage.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClientAfterInstallImage.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClientAfterInstallImage.On("ResetKubeadm").Return(sshclient.Output{})
diff --git a/pkg/services/baremetal/baremetal/baremetal_test.go b/pkg/services/baremetal/baremetal/baremetal_test.go
index 5d652f63..6773a5a4 100644
--- a/pkg/services/baremetal/baremetal/baremetal_test.go
+++ b/pkg/services/baremetal/baremetal/baremetal_test.go
@@ -337,7 +337,7 @@ var _ = Describe("chooseHost", func() {
 			testCaseChooseHostWithReason{
 				hosts:            []client.Object{&hostWithNonRaidWwnConfig},
 				expectedHostName: "",
-				expectedReason:   "No available host of 1 found: machine-should-use-swraid-but-only-0-RAID-WWN-in-hbmh: 1",
+				expectedReason:   "No available host of 1 found: machine-should-use-swraid-but-not-enough-RAID-WWNs-in-hbmh: 1",
 				swraid:           1,
 			}),
 		Entry("No host, because invalid RAID config (want no RAID)",
diff --git a/pkg/services/baremetal/client/ssh/ssh_client_test.go b/pkg/services/baremetal/client/ssh/ssh_client_test.go
index 55e7da57..6281269f 100644
--- a/pkg/services/baremetal/client/ssh/ssh_client_test.go
+++ b/pkg/services/baremetal/client/ssh/ssh_client_test.go
@@ -17,7 +17,13 @@ limitations under the License.
 // Package sshclient contains the interface to speak to bare metal servers with ssh.
 package sshclient
 
-import "testing"
+import (
+	_ "embed"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
 
 func Test_removeUselessLinesFromCloudInitOutput(t *testing.T) {
 	tests := []struct {
@@ -49,3 +55,50 @@ func Test_removeUselessLinesFromCloudInitOutput(t *testing.T) {
 		})
 	}
 }
+
+func TestOutput_String(t *testing.T) {
+	f := func(o Output, wantString string) {
+		require.Equal(t, wantString, o.String())
+	}
+	f(Output{
+		StdOut: "",
+		StdErr: "",
+		Err:    nil,
+	}, "")
+
+	f(Output{
+		StdOut: " mystdout ",
+		StdErr: "",
+		Err:    nil,
+	}, "mystdout")
+
+	f(Output{
+		StdOut: "",
+		StdErr: " mystderr ",
+		Err:    nil,
+	}, "mystderr")
+
+	f(Output{
+		StdOut: "",
+		StdErr: "",
+		Err:    fmt.Errorf(" some err "),
+	}, "some err")
+
+	f(Output{
+		StdOut: "mystdout",
+		StdErr: "",
+		Err:    fmt.Errorf("some err"),
+	}, "mystdout. Err: some err")
+
+	f(Output{
+		StdOut: "",
+		StdErr: "mystderr",
+		Err:    fmt.Errorf("some err"),
+	}, "mystderr. Err: some err")
+
+	f(Output{
+		StdOut: "mystdout",
+		StdErr: "mystderr",
+		Err:    fmt.Errorf("some err"),
+	}, "mystdout. Stderr: mystderr. Err: some err")
+}
diff --git a/pkg/services/baremetal/host/host_test.go b/pkg/services/baremetal/host/host_test.go
index aaf7ffd9..4fb8021c 100644
--- a/pkg/services/baremetal/host/host_test.go
+++ b/pkg/services/baremetal/host/host_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package host
 
 import (
+	"context"
 	"fmt"
 	"time"
 
@@ -1120,7 +1121,7 @@ var _ = Describe("actionRegistering", func() {
 		expectedErrorMessage      *string
 		swRaid                    bool
 	}
-
+	ctx := context.Background()
 	DescribeTable("actionRegistering",
 		func(tc testCaseActionRegistering) {
 			var host *infrav1.HetznerBareMetalHost
@@ -1155,7 +1156,7 @@ var _ = Describe("actionRegistering", func() {
 			sshMock := registeringSSHMock(tc.storageStdOut)
 			service := newTestService(host, nil, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionRegistering()
+			actResult := service.actionRegistering(ctx)
 			Expect(host.Spec.Status.HardwareDetails).ToNot(BeNil())
 			if tc.expectedErrorMessage != nil {
 				Expect(host.Spec.Status.ErrorMessage).To(Equal(*tc.expectedErrorMessage))
@@ -1231,7 +1232,7 @@ var _ = Describe("actionRegistering", func() {
 
 			service := newTestService(host, &robotMock, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionRegistering()
+			actResult := service.actionRegistering(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(actionContinue{}))
 			if tc.expectedErrorType != infrav1.ErrorType("") {
 				Expect(host.Spec.Status.ErrorType).To(Equal(tc.expectedErrorType))
@@ -1270,6 +1271,7 @@ name="eth0" model="Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express
 }
 
 var _ = Describe("actionRegistering check RAID", func() {
+	ctx := context.Background()
 	It("check RAID", func() {
 		sshMock := registeringSSHMock(`NAME="nvme2n1" TYPE="disk" MODEL="mymode." VENDOR="" SIZE="3068773888" WWN="wwn1" ROTA="0"
 		NAME="nvme2n2" TYPE="disk" MODEL="mymodel" VENDOR="" SIZE="3068773888" WWN="wwn2" ROTA="0"`)
@@ -1285,7 +1287,7 @@ var _ = Describe("actionRegistering check RAID", func() {
 		}
 		service := newTestService(host, nil, bmmock.NewSSHFactory(
 			sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
-		actResult := service.actionRegistering()
+		actResult := service.actionRegistering(ctx)
 
 		_, err := actResult.Result()
 		Expect(err).Should(BeNil())
@@ -1294,7 +1296,7 @@ var _ = Describe("actionRegistering check RAID", func() {
 		host.Spec.Status.InstallImage.Swraid = 0
 		host.Spec.RootDeviceHints.WWN = ""
 		host.Spec.RootDeviceHints.Raid.WWN = []string{"wwn1", "wwn2"}
-		actResult = service.actionRegistering()
+		actResult = service.actionRegistering(ctx)
 
 		_, err = actResult.Result()
 		Expect(err).Should(BeNil())
@@ -1390,6 +1392,7 @@ var _ = Describe("actionEnsureProvisioned", func() {
 
 	DescribeTable("actionEnsureProvisioned",
 		func(in testCaseActionEnsureProvisioned) {
+			ctx := context.Background()
 			var (
 				portAfterCloudInit    = 24
 				portAfterInstallImage = 23
@@ -1427,7 +1430,7 @@ var _ = Describe("actionEnsureProvisioned", func() {
 
 			service := newTestService(host, &robotMock, bmmock.NewSSHFactory(sshMock, oldSSHMock, sshMock), helpers.GetDefaultSSHSecret(osSSHKeyName, "default"), nil)
 
-			actResult := service.actionEnsureProvisioned()
+			actResult := service.actionEnsureProvisioned(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(in.expectedActionResult))
 			if in.expectedErrorType != infrav1.ErrorType("") {
 				Expect(host.Spec.Status.ErrorType).To(Equal(in.expectedErrorType))
@@ -1748,6 +1751,7 @@ var _ = Describe("actionProvisioned", func() {
 
 	DescribeTable("actionProvisioned",
 		func(tc testCaseActionProvisioned) {
+			ctx := context.Background()
 			host := helpers.BareMetalHost(
 				"test-host",
 				"default",
@@ -1774,7 +1778,7 @@ var _ = Describe("actionProvisioned", func() {
 
 			service := newTestService(host, nil, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), helpers.GetDefaultSSHSecret(osSSHKeyName, "default"), helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionProvisioned()
+			actResult := service.actionProvisioned(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(tc.expectedActionResult))
 			Expect(host.Spec.Status.Rebooted).To(Equal(tc.expectRebootInStatus))
 			Expect(host.HasRebootAnnotation()).To(Equal(tc.expectRebootAnnotation))
diff --git a/pkg/services/baremetal/host/state_machine_test.go b/pkg/services/baremetal/host/state_machine_test.go
index a2b5819b..50453ecb 100644
--- a/pkg/services/baremetal/host/state_machine_test.go
+++ b/pkg/services/baremetal/host/state_machine_test.go
@@ -189,7 +189,7 @@ var _ = Describe("updateSSHKey", func() {
 				"sshkey-name": []byte("my-name"),
 				"public-key":  []byte("my-public-key"),
 			},
-			currentState:         infrav1.StateProvisioning,
+			currentState:         infrav1.StateImageInstalling,
 			expectedActionResult: actionComplete{},
 			expectedNextState:    infrav1.StateImageInstalling,
 			expectedOSSecretData: map[string][]byte{
@@ -203,31 +203,6 @@ var _ = Describe("updateSSHKey", func() {
 				"public-key":  []byte("my-public-key"),
 			},
 		}),
-		Entry("rescue secret changed - state provisioning", testCaseUpdateSSHKey{
-			osSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-			rescueSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", rescueSSHKeyName)),
-				"sshkey-name": []byte("my-old-name"),
-				"public-key":  []byte("my-old-public-key"),
-			},
-			currentState:         infrav1.StateProvisioning,
-			expectedActionResult: actionComplete{},
-			expectedNextState:    infrav1.StateProvisioning,
-			expectedOSSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-			expectedRescueSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", rescueSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-		}),
 		Entry("rescue secret changed - state available", testCaseUpdateSSHKey{
 			osSecretData: map[string][]byte{
 				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
diff --git a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
index ef0e35c1..9689ca78 100644
--- a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
+++ b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
@@ -8,20 +8,17 @@ spec:
     wwn: "0x5002538c0004413f"
   maintenanceMode: false
   description: Test Machine 1670788
-
-# ---
-# Broken 2024-07-31
-# apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
-# kind: HetznerBareMetalHost
-# metadata:
-#   name: "bm-e2e-1716772"
-# spec:
-#   serverID: 1716772
-#   rootDeviceHints:
-#     wwn: "0x500a07510fe0ce34"
-#   maintenanceMode: false
-#   description: Test Machine 1716772
-
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: HetznerBareMetalHost
+metadata:
+  name: "bm-e2e-1716772"
+spec:
+  serverID: 1716772
+  rootDeviceHints:
+    wwn: "0x500a07510fe0ce34"
+  maintenanceMode: false
+  description: Test Machine 1716772
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost