diff --git a/tests/e2e/scenarios/bare-metal/run-test b/tests/e2e/scenarios/bare-metal/run-test
index a013ce55e0388..52aa415badf76 100755
--- a/tests/e2e/scenarios/bare-metal/run-test
+++ b/tests/e2e/scenarios/bare-metal/run-test
@@ -34,8 +34,10 @@ function cleanup() {
   echo "running dump-artifacts"
   ${REPO_ROOT}/tests/e2e/scenarios/bare-metal/dump-artifacts || true
 
-  echo "running cleanup"
-  ${REPO_ROOT}/tests/e2e/scenarios/bare-metal/cleanup || true
+  if [[ -z "${SKIP_CLEANUP:-}" ]]; then
+    echo "running cleanup"
+    ${REPO_ROOT}/tests/e2e/scenarios/bare-metal/cleanup || true
+  fi
 }
 
 if [[ -z "${SKIP_CLEANUP:-}" ]]; then
@@ -136,8 +138,14 @@ ssh -o StrictHostKeyChecking=accept-new -i ${REPO_ROOT}/.build/.ssh/id_ed25519 r
 ssh -o StrictHostKeyChecking=accept-new -i ${REPO_ROOT}/.build/.ssh/id_ed25519 root@10.123.45.10 touch    /mnt/disks/metal.k8s.local--events--0/mnt/please-create-new-cluster
 
 
-echo "Waiting 300 seconds for kube to start"
-sleep 300
+echo "Waiting for kube to start"
+# Wait for kube-apiserver to be ready, timeout after 10 minutes
+for i in {1..60}; do
+  if kubectl get nodes; then
+    break
+  fi
+  sleep 10
+done
 
 kubectl get nodes
 kubectl get pods -A
@@ -215,5 +223,7 @@ sleep 30
 kubectl get nodes
 kubectl get pods -A
 
+# Ensure the cluster passes validation
+${KOPS} validate cluster metal.k8s.local --wait=10m
 
 echo "Test successful"
\ No newline at end of file
diff --git a/upup/pkg/fi/cloudup/metal/cloud.go b/upup/pkg/fi/cloudup/metal/cloud.go
index 1b40d52150dc9..d8f543f72c9c8 100644
--- a/upup/pkg/fi/cloudup/metal/cloud.go
+++ b/upup/pkg/fi/cloudup/metal/cloud.go
@@ -75,7 +75,45 @@ func (c *Cloud) DetachInstance(instance *cloudinstances.CloudInstance) error {
 // GetCloudGroups returns a map of cloud instances that back a kops cluster.
 // Detached instances must be returned in the NeedUpdate slice.
 func (c *Cloud) GetCloudGroups(cluster *kops.Cluster, instancegroups []*kops.InstanceGroup, warnUnmatched bool, nodes []v1.Node) (map[string]*cloudinstances.CloudInstanceGroup, error) {
-	return nil, fmt.Errorf("method metal.Cloud::GetCloudGroups not implemented")
+	groups := make(map[string]*cloudinstances.CloudInstanceGroup)
+	for _, ig := range instancegroups {
+		cloudInstanceGroup := &cloudinstances.CloudInstanceGroup{
+			InstanceGroup: ig,
+		}
+		groups[ig.ObjectMeta.Name] = cloudInstanceGroup
+		for _, node := range nodes {
+			isControlPlaneNode := false
+			for k := range node.Labels {
+				if k == "node-role.kubernetes.io/control-plane" {
+					isControlPlaneNode = true
+				}
+
+			}
+
+			match := true
+			switch ig.Spec.Role {
+			case kops.InstanceGroupRoleControlPlane:
+				if !isControlPlaneNode {
+					match = false
+				}
+
+			case kops.InstanceGroupRoleNode:
+				if isControlPlaneNode {
+					match = false
+				}
+			}
+
+			if match {
+				cloudInstanceGroup.Ready = append(cloudInstanceGroup.Ready, &cloudinstances.CloudInstance{
+					ID:                 node.Name,
+					Node:               &node,
+					CloudInstanceGroup: cloudInstanceGroup,
+				})
+			}
+		}
+	}
+
+	return groups, nil
 }
 
 // Region returns the cloud region bound to the cloud instance.