Repository: metal3-io/baremetal-operator
Pull request #1435 (2023-11-07 16:56:08)

✨ Implement API version negotiation
Rather than bumping the microversion every time we need new features,
we will keep the baseline version and try to negotiate a higher version
in the provisioner. The new AvailableFeatures object provides
information about the supported features.

Includes:
- https://github.com/metal3-io/baremetal-operator/pull/1433


Patch:
diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go
index 3264b7b0..d9e7b0ad 100644
--- a/controllers/metal3.io/baremetalhost_controller.go
+++ b/controllers/metal3.io/baremetalhost_controller.go
@@ -208,7 +208,7 @@ func (r *BareMetalHostReconciler) Reconcile(ctx context.Context, request ctrl.Re
 		return ctrl.Result{}, errors.Wrap(err, "failed to create provisioner")
 	}
 
-	ready, err := prov.IsReady()
+	ready, err := prov.TryInit()
 	if err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "failed to check services availability")
 	}
diff --git a/controllers/metal3.io/bmceventsubscription_controller.go b/controllers/metal3.io/bmceventsubscription_controller.go
index 7d9fd943..2a2fa6b4 100644
--- a/controllers/metal3.io/bmceventsubscription_controller.go
+++ b/controllers/metal3.io/bmceventsubscription_controller.go
@@ -224,7 +224,7 @@ func (r *BMCEventSubscriptionReconciler) getProvisioner(request ctrl.Request, ho
 		return prov, ready, errors.Wrap(err, "failed to create provisioner")
 	}
 
-	ready, err = prov.IsReady()
+	ready, err = prov.TryInit()
 	if err != nil {
 		return prov, ready, errors.Wrap(err, "failed to check services availability")
 	}
diff --git a/controllers/metal3.io/hostfirmwaresettings_controller.go b/controllers/metal3.io/hostfirmwaresettings_controller.go
index 026ecc8b..a2f97b23 100644
--- a/controllers/metal3.io/hostfirmwaresettings_controller.go
+++ b/controllers/metal3.io/hostfirmwaresettings_controller.go
@@ -151,7 +151,7 @@ func (r *HostFirmwareSettingsReconciler) Reconcile(ctx context.Context, req ctrl
 		return ctrl.Result{}, errors.Wrap(err, "failed to create provisioner")
 	}
 
-	ready, err := prov.IsReady()
+	ready, err := prov.TryInit()
 	if err != nil || !ready {
 		var msg string
 		if err == nil {
diff --git a/go.mod b/go.mod
index 395300d0..0493ca4b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.20
 require (
 	github.com/go-logr/logr v1.3.0
 	github.com/google/safetext v0.0.0-20230106111101-7156a760e523
-	github.com/gophercloud/gophercloud v1.5.1-0.20230912084133-c79ed6d3b371
+	github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1
 	github.com/metal3-io/baremetal-operator/apis v0.4.0
 	github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.4.0
 	github.com/onsi/gomega v1.29.0
diff --git a/go.sum b/go.sum
index 627d3f80..b3cd5f48 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ github.com/google/safetext v0.0.0-20230106111101-7156a760e523 h1:i4NsbmB9pD5+Ggp
 github.com/google/safetext v0.0.0-20230106111101-7156a760e523/go.mod h1:mJNEy0r5YPHC7ChQffpOszlGB4L1iqjXWpIEKcFpr9s=
 github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gophercloud/gophercloud v1.5.1-0.20230912084133-c79ed6d3b371 h1:/4TDrmUM3o52/9J3Pn4mZ3VP7CYiFkDKRNuWyxLkpp4=
-github.com/gophercloud/gophercloud v1.5.1-0.20230912084133-c79ed6d3b371/go.mod h1:vYRGjRkTeMAwdvQFTVuPGfRWv4dc3J2QHy9H3bHD6Pc=
+github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1 h1:4crYiUFYzhxv60/bsLnyc7esUsOqQl3DgErAGNsOgL8=
+github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1/go.mod h1:99P1GdytPKaSR7/ugQJo1dX6th5FlHdXXvKqvRJQfO4=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go
index 5a54de4d..3d575524 100644
--- a/pkg/provisioner/demo/demo.go
+++ b/pkg/provisioner/demo/demo.go
@@ -349,8 +349,8 @@ func (p *demoProvisioner) PowerOff(_ metal3api.RebootMode, _ bool) (result provi
 	// return result, nil
 }
 
-// IsReady always returns true for the demo provisioner
-func (p *demoProvisioner) IsReady() (result bool, err error) {
+// TryInit always returns true for the demo provisioner
+func (p *demoProvisioner) TryInit() (result bool, err error) {
 	return true, nil
 }
 
diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go
index e762fc5c..9b869d12 100644
--- a/pkg/provisioner/fixture/fixture.go
+++ b/pkg/provisioner/fixture/fixture.go
@@ -325,8 +325,8 @@ func (p *fixtureProvisioner) PowerOff(_ metal3api.RebootMode, _ bool) (result pr
 	return result, nil
 }
 
-// IsReady returns the current availability status of the provisioner
-func (p *fixtureProvisioner) IsReady() (result bool, err error) {
+// TryInit returns the current availability status of the provisioner
+func (p *fixtureProvisioner) TryInit() (result bool, err error) {
 	p.log.Info("checking provisioner status")
 
 	if p.state.BecomeReadyCounter > 0 {
diff --git a/pkg/provisioner/ironic/clients/client.go b/pkg/provisioner/ironic/clients/client.go
index ad2821ed..9753b8d4 100644
--- a/pkg/provisioner/ironic/clients/client.go
+++ b/pkg/provisioner/ironic/clients/client.go
@@ -27,7 +27,7 @@ type TLSConfig struct {
 	SkipClientSANVerify   bool
 }
 
-func updateHTTPClient(client *gophercloud.ServiceClient, tlsConf TLSConfig) (*gophercloud.ServiceClient, error) {
+func updateHTTPClient(client *gophercloud.ServiceClient, tlsConf TLSConfig) error {
 	tlsInfo := transport.TLSInfo{
 		TrustedCAFile:       tlsConf.TrustedCAFile,
 		CertFile:            tlsConf.ClientCertificateFile,
@@ -39,21 +39,21 @@ func updateHTTPClient(client *gophercloud.ServiceClient, tlsConf TLSConfig) (*go
 		if os.IsNotExist(err) {
 			tlsInfo.TrustedCAFile = ""
 		} else {
-			return client, err
+			return err
 		}
 	}
 	if _, err := os.Stat(tlsConf.ClientCertificateFile); err != nil {
 		if os.IsNotExist(err) {
 			tlsInfo.CertFile = ""
 		} else {
-			return client, err
+			return err
 		}
 	}
 	if _, err := os.Stat(tlsConf.ClientPrivateKeyFile); err != nil {
 		if os.IsNotExist(err) {
 			tlsInfo.KeyFile = ""
 		} else {
-			return client, err
+			return err
 		}
 	}
 	if tlsInfo.CertFile != "" && tlsInfo.KeyFile != "" {
@@ -62,13 +62,13 @@ func updateHTTPClient(client *gophercloud.ServiceClient, tlsConf TLSConfig) (*go
 
 	tlsTransport, err := transport.NewTransport(tlsInfo, tlsConnectionTimeout)
 	if err != nil {
-		return client, err
+		return err
 	}
 	c := http.Client{
 		Transport: tlsTransport,
 	}
 	client.HTTPClient = c
-	return client, nil
+	return nil
 }
 
 // IronicClient creates a client for Ironic
@@ -91,10 +91,8 @@ func IronicClient(ironicEndpoint string, auth AuthConfig, tls TLSConfig) (client
 		return
 	}
 
-	// Ensure we have a microversion high enough to get the features
-	// we need. Update docs/configuration.md when updating the version.
-	// Version 1.81 allows retrival of Node inventory
-	client.Microversion = "1.81"
+	client.Microversion = baseline
 
-	return updateHTTPClient(client, tls)
+	err = updateHTTPClient(client, tls)
+	return
 }
diff --git a/pkg/provisioner/ironic/clients/features.go b/pkg/provisioner/ironic/clients/features.go
new file mode 100644
index 00000000..46452d65
--- /dev/null
+++ b/pkg/provisioner/ironic/clients/features.go
@@ -0,0 +1,57 @@
+package clients
+
+import (
+	"fmt"
+
+	"github.com/go-logr/logr"
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/utils"
+)
+
+// AvailableFeatures represents features that Ironic API provides.
+// See https://docs.openstack.org/ironic/latest/contributor/webapi-version-history.html
+type AvailableFeatures struct {
+	MaxVersion int
+}
+
+// The minimum microversion that BMO can work with. We need to be really
+// conservative when updating this value since doing so unconditionally breaks
+// operators of older Ironic, even if the feature we need is optional.
+// Update docs/configuration.md when updating the version.
+// Version 1.81 allows retrival of Node inventory
+const baseline = "1.81"
+
+func GetAvailableFeatures(client *gophercloud.ServiceClient) (features AvailableFeatures, err error) {
+	mvs, err := utils.GetSupportedMicroversions(client)
+	if err != nil {
+		return
+	}
+
+	if mvs.MaxMajor != 1 || mvs.MaxMinor < 81 {
+		err = fmt.Errorf("Ironic API 1.81 or newer is required, got %d.%d", mvs.MaxMajor, mvs.MaxMinor)
+		return
+	}
+
+	features.MaxVersion = mvs.MaxMinor
+	return
+}
+
+func (af AvailableFeatures) Log(logger logr.Logger) {
+	// NOTE(dtantsur): update this every time we have more features of interest
+	logger.Info("supported Ironic API features",
+		"maxVersion", fmt.Sprintf("1.%d", af.MaxVersion),
+		"chosenVersion", af.ChooseMicroversion(),
+		"firmwareUpgrades", af.HasFirmwareUpgrades())
+}
+
+func (af AvailableFeatures) HasFirmwareUpgrades() bool {
+	return af.MaxVersion >= 86
+}
+
+func (af AvailableFeatures) ChooseMicroversion() string {
+	if af.HasFirmwareUpgrades() {
+		return "1.86"
+	}
+
+	return baseline
+}
diff --git a/pkg/provisioner/ironic/dependencies.go b/pkg/provisioner/ironic/dependencies.go
index 06d3662f..72643a8e 100644
--- a/pkg/provisioner/ironic/dependencies.go
+++ b/pkg/provisioner/ironic/dependencies.go
@@ -1,57 +1,30 @@
 package ironic
 
 import (
-	"strings"
-	"time"
-
-	"github.com/go-logr/logr"
-	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/drivers"
 	"github.com/gophercloud/gophercloud/pagination"
-)
 
-const (
-	checkRequeueDelay = time.Second * 10
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
 )
 
-type ironicDependenciesChecker struct {
-	client *gophercloud.ServiceClient
-	log    logr.Logger
-}
-
-func newIronicDependenciesChecker(client *gophercloud.ServiceClient, log logr.Logger) *ironicDependenciesChecker {
-	return &ironicDependenciesChecker{
-		client: client,
-		log:    log,
-	}
-}
-
-func (i *ironicDependenciesChecker) IsReady() (result bool, err error) {
-	ready := i.checkEndpoint(i.client)
-	if ready {
-		ready, err = i.checkIronicConductor()
-	}
-	return ready, err
-}
-
-func (i *ironicDependenciesChecker) checkEndpoint(client *gophercloud.ServiceClient) (ready bool) {
+// TryInit checks if the provisioning backend is available
+func (p *ironicProvisioner) TryInit() (ready bool, err error) {
+	p.debugLog.Info("verifying ironic provisioner dependencies")
 
-	// NOTE: Some versions of Ironic inspector returns 404 for /v1/ but 200 for /v1,
-	// which seems to be the default behavior for Flask. Remove the trailing slash
-	// from the client endpoint.
-	endpoint := strings.TrimSuffix(client.Endpoint, "/")
-
-	_, err := client.Get(endpoint, nil, nil)
+	p.availableFeatures, err = clients.GetAvailableFeatures(p.client)
 	if err != nil {
-		i.log.Info("error caught while checking endpoint", "endpoint", client.Endpoint, "error", err)
+		p.log.Info("error caught while checking endpoint, will retry", "endpoint", p.client.Endpoint, "error", err)
+		return false, nil
 	}
 
-	return err == nil
-}
+	p.client.Microversion = p.availableFeatures.ChooseMicroversion()
+	p.availableFeatures.Log(p.debugLog)
 
-func (i *ironicDependenciesChecker) checkIronicConductor() (ready bool, err error) {
+	return p.checkIronicConductor()
+}
 
-	pager := drivers.ListDrivers(i.client, drivers.ListDriversOpts{
+func (p *ironicProvisioner) checkIronicConductor() (ready bool, err error) {
+	pager := drivers.ListDrivers(p.client, drivers.ListDriversOpts{
 		Detail: false,
 	})
 	err = pager.Err
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 2dc7e6d1..6d8d7e21 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -20,6 +20,7 @@ import (
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 	"github.com/metal3-io/baremetal-operator/pkg/hardwareutils/bmc"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/devicehints"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails"
 )
@@ -103,6 +104,8 @@ type ironicProvisioner struct {
 	debugLog logr.Logger
 	// an event publisher for recording significant events
 	publisher provisioner.EventPublisher
+	// available API features
+	availableFeatures clients.AvailableFeatures
 }
 
 func (p *ironicProvisioner) bmcAccess() (bmc.AccessDetails, error) {
@@ -1979,14 +1982,6 @@ func ironicNodeName(objMeta metav1.ObjectMeta) string {
 	return objMeta.Namespace + nameSeparator + objMeta.Name
 }
 
-// IsReady checks if the provisioning backend is available
-func (p *ironicProvisioner) IsReady() (result bool, err error) {
-	p.debugLog.Info("verifying ironic provisioner dependencies")
-
-	checker := newIronicDependenciesChecker(p.client, p.log)
-	return checker.IsReady()
-}
-
 func (p *ironicProvisioner) HasCapacity() (result bool, err error) {
 	bmcAccess, err := p.bmcAccess()
 	if err != nil {
diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go
index fa2774d0..3b791de5 100644
--- a/pkg/provisioner/provisioner.go
+++ b/pkg/provisioner/provisioner.go
@@ -184,9 +184,9 @@ type Provisioner interface {
 	// if a hard reboot (force power off) is required - true if so.
 	PowerOff(rebootMode metal3api.RebootMode, force bool) (result Result, err error)
 
-	// IsReady checks if the provisioning backend is available to accept
-	// all the incoming requests.
-	IsReady() (result bool, err error)
+	// TryInit checks if the provisioning backend is available to accept
+	// all the incoming requests and configures the available features.
+	TryInit() (ready bool, err error)
 
 	// HasCapacity checks if the backend has a free (de)provisioning slot for the current host
 	HasCapacity() (result bool, err error)


Test patch:
diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go
index 4478aa0f..0d0e94a9 100644
--- a/controllers/metal3.io/host_state_machine_test.go
+++ b/controllers/metal3.io/host_state_machine_test.go
@@ -1384,7 +1384,7 @@ func (m *mockProvisioner) PowerOff(_ metal3api.RebootMode, _ bool) (result provi
 	return m.getNextResultByMethod("PowerOff"), err
 }
 
-func (m *mockProvisioner) IsReady() (result bool, err error) {
+func (m *mockProvisioner) TryInit() (result bool, err error) {
 	return
 }
 
diff --git a/pkg/provisioner/ironic/adopt_test.go b/pkg/provisioner/ironic/adopt_test.go
index d203528f..0fcd5d08 100644
--- a/pkg/provisioner/ironic/adopt_test.go
+++ b/pkg/provisioner/ironic/adopt_test.go
@@ -27,7 +27,7 @@ func TestAdopt(t *testing.T) {
 	}{
 		{
 			name: "node-in-enroll",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Enroll),
 				UUID:           nodeUUID,
 			}),
@@ -47,7 +47,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-adopting",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Adopting),
 				UUID:           nodeUUID,
 			}),
@@ -57,7 +57,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-verifying",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Verifying),
 				UUID:           nodeUUID,
 			}),
@@ -67,7 +67,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-AdoptFail",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.AdoptFail),
 				UUID:           nodeUUID,
 			}),
@@ -88,7 +88,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-Active",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Active),
 				UUID:           nodeUUID,
 			}),
@@ -97,7 +97,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-Maintenance",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Active),
 				UUID:           nodeUUID,
 				Maintenance:    true,
@@ -109,7 +109,7 @@ func TestAdopt(t *testing.T) {
 		},
 		{
 			name: "node-in-Fault",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Active),
 				UUID:           nodeUUID,
 				Maintenance:    true,
diff --git a/pkg/provisioner/ironic/configdrive_test.go b/pkg/provisioner/ironic/configdrive_test.go
index d2f6a1d0..9bb8d472 100644
--- a/pkg/provisioner/ironic/configdrive_test.go
+++ b/pkg/provisioner/ironic/configdrive_test.go
@@ -122,7 +122,7 @@ func TestEmpty(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			ironic := testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic := testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Active),
 				UUID:           nodeUUID,
 			})
diff --git a/pkg/provisioner/ironic/delete_test.go b/pkg/provisioner/ironic/delete_test.go
index 84ee54c1..82d4c1f0 100644
--- a/pkg/provisioner/ironic/delete_test.go
+++ b/pkg/provisioner/ironic/delete_test.go
@@ -90,13 +90,13 @@ func deleteTest(t *testing.T, detach bool) {
 			name: "host-not-found",
 
 			hostName: "worker-0",
-			ironic:   testserver.NewIronic(t).Ready().NodeError(nodeUUID, http.StatusGatewayTimeout),
+			ironic:   testserver.NewIronic(t).NodeError(nodeUUID, http.StatusGatewayTimeout),
 
 			expectedError: "failed to find node by ID 33ce8659-7400-4c68-9535-d10766f07a58: Gateway Timeout.*",
 		},
 		{
 			name:   "not-ironic-node",
-			ironic: testserver.NewIronic(t).Ready().NoNode(nodeUUID).NoNode("myns" + nameSeparator + "myhost").NoNode("myhost"),
+			ironic: testserver.NewIronic(t).NoNode(nodeUUID).NoNode("myns" + nameSeparator + "myhost").NoNode("myhost"),
 		},
 		{
 			name: "available-node",
diff --git a/pkg/provisioner/ironic/inspecthardware_test.go b/pkg/provisioner/ironic/inspecthardware_test.go
index ab3d4db7..f643e057 100644
--- a/pkg/provisioner/ironic/inspecthardware_test.go
+++ b/pkg/provisioner/ironic/inspecthardware_test.go
@@ -89,7 +89,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "introspection-status-retry-on-wait",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: "inspect wait",
 			}),
@@ -99,7 +99,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "introspection-status-retry-on-inspecting",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: "inspecting",
 			}),
@@ -109,7 +109,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "introspection-failed",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: "inspect failed",
 				LastError:      "Timeout",
@@ -119,7 +119,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "introspection-aborted",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: "inspect failed",
 				LastError:      "Inspection was aborted by request.",
@@ -135,7 +135,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "inspection-in-progress - forceReboot",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: string(nodes.InspectWait),
 			}).WithNodeStatesProvisionUpdate(nodeUUID),
@@ -147,7 +147,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "inspection-failed",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: string(nodes.InspectFail),
 			}),
@@ -156,7 +156,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "inspection-failed force",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: string(nodes.InspectFail),
 			}).NodeUpdate(nodes.Node{
@@ -172,7 +172,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "inspection-forceReboot",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: string(nodes.InspectWait),
 			}).WithNodeStatesProvisionUpdate(nodeUUID),
@@ -184,7 +184,7 @@ func TestInspectHardware(t *testing.T) {
 		},
 		{
 			name: "inspection-complete",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:           nodeUUID,
 				ProvisionState: string(nodes.Manageable),
 			}).WithInventory(nodeUUID, nodes.InventoryData{
diff --git a/pkg/provisioner/ironic/power_test.go b/pkg/provisioner/ironic/power_test.go
index 2b097e24..98df3ea3 100644
--- a/pkg/provisioner/ironic/power_test.go
+++ b/pkg/provisioner/ironic/power_test.go
@@ -30,14 +30,14 @@ func TestPowerOn(t *testing.T) {
 	}{
 		{
 			name: "node-already-power-on",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState: powerOn,
 				UUID:       nodeUUID,
 			}),
 		},
 		{
 			name: "waiting-for-target-power-on",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:       powerOff,
 				TargetPowerState: powerOn,
 				UUID:             nodeUUID,
@@ -57,7 +57,7 @@ func TestPowerOn(t *testing.T) {
 		},
 		{
 			name: "power-on wait for Provisioning state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:           powerOff,
 				TargetPowerState:     powerOff,
 				TargetProvisionState: string(nodes.TargetDeleted),
@@ -68,7 +68,7 @@ func TestPowerOn(t *testing.T) {
 		},
 		{
 			name: "power-on wait for locked host",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:           powerOff,
 				TargetPowerState:     powerOff,
 				TargetProvisionState: "",
@@ -79,7 +79,7 @@ func TestPowerOn(t *testing.T) {
 		},
 		{
 			name: "power-on with LastError",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:       powerOff,
 				TargetPowerState: powerOff,
 				UUID:             nodeUUID,
@@ -92,7 +92,7 @@ func TestPowerOn(t *testing.T) {
 		{
 			name:  "power-on with LastError",
 			force: true,
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:       powerOff,
 				TargetPowerState: powerOff,
 				UUID:             nodeUUID,
@@ -153,14 +153,14 @@ func TestPowerOff(t *testing.T) {
 	}{
 		{
 			name: "node-already-power-off",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState: powerOff,
 				UUID:       nodeUUID,
 			}),
 		},
 		{
 			name: "waiting-for-target-power-off",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:       powerOn,
 				TargetPowerState: powerOff,
 				UUID:             nodeUUID,
@@ -192,7 +192,7 @@ func TestPowerOff(t *testing.T) {
 		},
 		{
 			name: "power-off wait for Provisioning state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:           powerOn,
 				TargetProvisionState: string(nodes.TargetDeleted),
 				UUID:                 nodeUUID,
@@ -202,7 +202,7 @@ func TestPowerOff(t *testing.T) {
 		},
 		{
 			name: "power-off wait for locked host",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				PowerState:           powerOn,
 				TargetProvisionState: "",
 				UUID:                 nodeUUID,
@@ -297,7 +297,7 @@ func TestSoftPowerOffFallback(t *testing.T) {
 		PowerState: powerOn,
 		UUID:       nodeUUID,
 	}
-	ironic := testserver.NewIronic(t).Ready().Node(node).WithNodeStatesPowerUpdate(nodeUUID, http.StatusBadRequest)
+	ironic := testserver.NewIronic(t).Node(node).WithNodeStatesPowerUpdate(nodeUUID, http.StatusBadRequest)
 	ironic.Start()
 	defer ironic.Stop()
 
diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go
index c39b885e..121b0d99 100644
--- a/pkg/provisioner/ironic/provision_test.go
+++ b/pkg/provisioner/ironic/provision_test.go
@@ -97,7 +97,7 @@ func TestProvision(t *testing.T) {
 		},
 		{
 			name: "active state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Active),
 				UUID:           nodeUUID,
 			}),
@@ -106,7 +106,7 @@ func TestProvision(t *testing.T) {
 		},
 		{
 			name: "other state: Cleaning",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Cleaning),
 				UUID:           nodeUUID,
 			}),
@@ -115,7 +115,7 @@ func TestProvision(t *testing.T) {
 		},
 		{
 			name: "other state: Deploy Wait",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.DeployWait),
 				UUID:           nodeUUID,
 			}),
@@ -243,7 +243,7 @@ func TestDeprovision(t *testing.T) {
 		},
 		{
 			name: "deleting state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Deleting),
 				UUID:           nodeUUID,
 			}),
@@ -252,7 +252,7 @@ func TestDeprovision(t *testing.T) {
 		},
 		{
 			name: "cleaning state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.Cleaning),
 				UUID:           nodeUUID,
 			}),
@@ -261,7 +261,7 @@ func TestDeprovision(t *testing.T) {
 		},
 		{
 			name: "cleanWait state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				ProvisionState: string(nodes.CleanWait),
 				UUID:           nodeUUID,
 			}),
diff --git a/pkg/provisioner/ironic/ready_test.go b/pkg/provisioner/ironic/ready_test.go
index a7007fcf..b80b41f8 100644
--- a/pkg/provisioner/ironic/ready_test.go
+++ b/pkg/provisioner/ironic/ready_test.go
@@ -23,14 +23,14 @@ func TestProvisionerIsReady(t *testing.T) {
 	}{
 		{
 			name:                "IsReady",
-			ironic:              testserver.NewIronic(t).Ready().WithDrivers(),
-			expectedIronicCalls: "/v1;/v1/drivers;",
+			ironic:              testserver.NewIronic(t).WithDrivers(),
+			expectedIronicCalls: "/v1/;/v1/drivers;",
 			expectedIsReady:     true,
 		},
 		{
 			name:                "NoDriversLoaded",
-			ironic:              testserver.NewIronic(t).Ready(),
-			expectedIronicCalls: "/v1;/v1/drivers;",
+			ironic:              testserver.NewIronic(t),
+			expectedIronicCalls: "/v1/;/v1/drivers;",
 		},
 		{
 			name:            "IronicDown",
@@ -40,13 +40,13 @@ func TestProvisionerIsReady(t *testing.T) {
 			name:                "IronicNotOk",
 			ironic:              testserver.NewIronic(t).NotReady(http.StatusInternalServerError),
 			expectedIsReady:     false,
-			expectedIronicCalls: "/v1;",
+			expectedIronicCalls: "/v1/;",
 		},
 		{
 			name:                "IronicNotOkAndNotExpected",
 			ironic:              testserver.NewIronic(t).NotReady(http.StatusBadGateway),
 			expectedIsReady:     false,
-			expectedIronicCalls: "/v1;",
+			expectedIronicCalls: "/v1/;",
 		},
 	}
 
@@ -65,7 +65,7 @@ func TestProvisionerIsReady(t *testing.T) {
 				t.Fatalf("could not create provisioner: %s", err)
 			}
 
-			ready, err := prov.IsReady()
+			ready, err := prov.TryInit()
 			if err != nil {
 				t.Fatalf("could not determine ready state: %s", err)
 			}
diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go
index a287228c..85a6dcb3 100644
--- a/pkg/provisioner/ironic/testserver/ironic.go
+++ b/pkg/provisioner/ironic/testserver/ironic.go
@@ -20,15 +20,28 @@ type IronicMock struct {
 
 // NewIronic builds an ironic mock server
 func NewIronic(t *testing.T) *IronicMock {
-
+	server := New(t, "ironic").AddDefaultResponse("/v1/?", "", http.StatusOK, versionedRootResult)
 	return &IronicMock{
-		MockServer:   New(t, "ironic"),
+		MockServer:   server,
 		CreatedNodes: 0,
 	}
 }
 
 const validateResult = `{"boot": {"result": true}, "deploy": {"result": true}, "power": {"result": true}}`
 
+// NOTE(dtantsur): the actual result is much longer, but we only potentially care about versions.
+const versionedRootResult = `
+{
+ "id": "v1",
+ "version": {
+    "id": "v1",
+    "links": [ { "href": "/v1/", "rel": "self" } ],
+    "status": "CURRENT",
+    "min_version": "1.1",
+    "version": "1.86"
+  }
+}`
+
 // WithDefaultResponses sets a valid answer for all the API calls
 func (m *IronicMock) WithDefaultResponses() *IronicMock {
 	m.AddDefaultResponseJSON("/v1/nodes/{id}", "", http.StatusOK, nodes.Node{
@@ -38,7 +51,6 @@ func (m *IronicMock) WithDefaultResponses() *IronicMock {
 	m.AddDefaultResponse("/v1/nodes/{id}/states/power", "", http.StatusAccepted, "{}")
 	m.AddDefaultResponse("/v1/nodes/{id}/states/raid", "", http.StatusNoContent, "{}")
 	m.AddDefaultResponse("/v1/nodes/{id}/validate", "", http.StatusOK, validateResult)
-	m.Ready()
 
 	return m
 }
@@ -51,15 +63,9 @@ func (m *IronicMock) Endpoint() string {
 	return m.MockServer.Endpoint()
 }
 
-// Ready configures the server with a valid response for /v1
-func (m *IronicMock) Ready() *IronicMock {
-	m.ResponseWithCode("/v1", "{}", http.StatusOK)
-	return m
-}
-
-// NotReady configures the server with an error response for /v1
+// NotReady configures the server with an error response for /v1/ overriding the default response
 func (m *IronicMock) NotReady(errorCode int) *IronicMock {
-	m.ErrorResponse("/v1", errorCode)
+	m.ErrorResponse("/v1/", errorCode)
 	return m
 }
 
diff --git a/pkg/provisioner/ironic/updatehardwarestate_test.go b/pkg/provisioner/ironic/updatehardwarestate_test.go
index ab2b13b6..c7700821 100644
--- a/pkg/provisioner/ironic/updatehardwarestate_test.go
+++ b/pkg/provisioner/ironic/updatehardwarestate_test.go
@@ -29,14 +29,14 @@ func TestUpdateHardwareState(t *testing.T) {
 	}{
 		{
 			name: "unknown-power-state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID: nodeUUID,
 			}),
 			expectUnreadablePower: true,
 		},
 		{
 			name: "updated-power-on-state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:       nodeUUID,
 				PowerState: "power on",
 			}),
@@ -44,7 +44,7 @@ func TestUpdateHardwareState(t *testing.T) {
 		},
 		{
 			name: "not-updated-power-on-state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:       nodeUUID,
 				PowerState: "power on",
 			}),
@@ -52,7 +52,7 @@ func TestUpdateHardwareState(t *testing.T) {
 		},
 		{
 			name: "updated-power-off-state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:       nodeUUID,
 				PowerState: "power off",
 			}),
@@ -60,7 +60,7 @@ func TestUpdateHardwareState(t *testing.T) {
 		},
 		{
 			name: "not-updated-power-off-state",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:       nodeUUID,
 				PowerState: "power off",
 			}),
@@ -68,7 +68,7 @@ func TestUpdateHardwareState(t *testing.T) {
 		},
 		{
 			name: "no-power",
-			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+			ironic: testserver.NewIronic(t).Node(nodes.Node{
 				UUID:       nodeUUID,
 				PowerState: "None",
 			}),
@@ -79,7 +79,7 @@ func TestUpdateHardwareState(t *testing.T) {
 			name: "node-not-found",
 
 			hostName: "worker-0",
-			ironic:   testserver.NewIronic(t).Ready().NodeError(nodeUUID, http.StatusGatewayTimeout),
+			ironic:   testserver.NewIronic(t).NodeError(nodeUUID, http.StatusGatewayTimeout),
 
 			expectedError: "failed to find node by ID 33ce8659-7400-4c68-9535-d10766f07a58: Gateway Timeout.*",
 
@@ -89,7 +89,7 @@ func TestUpdateHardwareState(t *testing.T) {
 			name: "node-not-found-by-name",
 
 			hostName: "worker-0",
-			ironic:   testserver.NewIronic(t).Ready().NoNode(nodeUUID).NodeError("myns"+nameSeparator+"myhost", http.StatusGatewayTimeout),
+			ironic:   testserver.NewIronic(t).NoNode(nodeUUID).NodeError("myns"+nameSeparator+"myhost", http.StatusGatewayTimeout),
 
 			expectedError: "Host not registered",
 
@@ -97,7 +97,7 @@ func TestUpdateHardwareState(t *testing.T) {
 		},
 		{
 			name:   "not-ironic-node",
-			ironic: testserver.NewIronic(t).Ready().NoNode(nodeUUID).NoNode("myns" + nameSeparator + "myhost").NoNode("myhost"),
+			ironic: testserver.NewIronic(t).NoNode(nodeUUID).NoNode("myns" + nameSeparator + "myhost").NoNode("myhost"),
 
 			expectedError: "Host not registered",
 
diff --git a/pkg/provisioner/ironic/validatemanagementaccess_test.go b/pkg/provisioner/ironic/validatemanagementaccess_test.go
index 819f88ec..5a73c0c3 100644
--- a/pkg/provisioner/ironic/validatemanagementaccess_test.go
+++ b/pkg/provisioner/ironic/validatemanagementaccess_test.go
@@ -25,7 +25,7 @@ func TestValidateManagementAccessNoMAC(t *testing.T) {
 	host.Spec.BootMACAddress = ""
 	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
 
-	ironic := testserver.NewIronic(t).Ready().NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
+	ironic := testserver.NewIronic(t).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
 	ironic.Start()
 	defer ironic.Stop()
 
@@ -49,7 +49,7 @@ func TestValidateManagementAccessMACOptional(t *testing.T) {
 	host.Spec.BootMACAddress = ""
 
 	// Set up ironic server to return the node
-	ironic := testserver.NewIronic(t).Ready().
+	ironic := testserver.NewIronic(t).
 		Node(nodes.Node{
 			Name: host.Namespace + nameSeparator + host.Name,
 			UUID: host.Status.Provisioning.ID,
@@ -86,7 +86,7 @@ func TestValidateManagementAccessCreateNodeNoImage(t *testing.T) {
 		createdNode = &node
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
 	ironic.AddDefaultResponse("/v1/nodes/node-0", "PATCH", http.StatusOK, "{}")
 	ironic.Start()
 	defer ironic.Stop()
@@ -120,7 +120,7 @@ func TestValidateManagementAccessCreateWithImage(t *testing.T) {
 		createdNode = &node
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
 	ironic.AddDefaultResponse("/v1/nodes/node-0", "PATCH", http.StatusOK, "{}")
 	ironic.Start()
 	defer ironic.Stop()
@@ -156,7 +156,7 @@ func TestValidateManagementAccessCreateWithLiveIso(t *testing.T) {
 		createdNode = &node
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).NoNode(host.Namespace + nameSeparator + host.Name).NoNode(host.Name)
 	ironic.AddDefaultResponse("/v1/nodes/node-0", "PATCH", http.StatusOK, "{}")
 	ironic.Start()
 	defer ironic.Stop()
@@ -190,7 +190,7 @@ func TestValidateManagementAccessExistingNode(t *testing.T) {
 		t.Fatal("create callback should not be invoked for existing node")
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(nodes.Node{
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(nodes.Node{
 		Name: host.Namespace + nameSeparator + host.Name,
 		UUID: "uuid",
 	}).NodeUpdate(
@@ -289,7 +289,7 @@ func TestValidateManagementAccessExistingNodeContinue(t *testing.T) {
 				t.Fatal("create callback should not be invoked for existing node")
 			}
 
-			ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(nodes.Node{
+			ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(nodes.Node{
 				Name:           host.Namespace + nameSeparator + host.Name,
 				UUID:           "uuid", // to match status in host
 				ProvisionState: string(status),
@@ -451,7 +451,7 @@ func TestValidateManagementAccessExistingSteadyStateNoUpdate(t *testing.T) {
 			if provisionState == "" {
 				provisionState = string(nodes.Manageable)
 			}
-			ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(nodes.Node{
+			ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(nodes.Node{
 				Name:            host.Namespace + nameSeparator + host.Name,
 				UUID:            "uuid", // to match status in host
 				ProvisionState:  provisionState,
@@ -517,7 +517,7 @@ func TestValidateManagementAccessExistingNodeWaiting(t *testing.T) {
 					"test_port":      "42",
 				},
 			}
-			ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(node).NodeUpdate(nodes.Node{
+			ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(node).NodeUpdate(nodes.Node{
 				UUID: "uuid",
 			}).WithNodeStatesProvisionUpdate(node.UUID)
 			ironic.Start()
@@ -615,7 +615,7 @@ func TestValidateManagementAccessLinkExistingIronicNodeByMAC(t *testing.T) {
 		t.Fatal("create callback should not be invoked for existing node")
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
 	ironic.AddDefaultResponse("/v1/nodes/myns"+nameSeparator+"myhost", "GET", http.StatusNotFound, "")
 	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
 	ironic.AddDefaultResponse("/v1/nodes/"+existingNode.UUID, "PATCH", http.StatusOK, "{\"uuid\": \"33ce8659-7400-4c68-9535-d10766f07a58\"}")
@@ -657,7 +657,7 @@ func TestValidateManagementAccessExistingPortWithWrongUUID(t *testing.T) {
 		t.Fatal("create callback should not be invoked for existing node")
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
 	ironic.AddDefaultResponse("/v1/nodes/myns"+nameSeparator+"myhost", "GET", http.StatusNotFound, "")
 	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
 	ironic.AddDefaultResponse("/v1/nodes/random-wrong-id", "GET", http.StatusNotFound, "")
@@ -701,7 +701,7 @@ func TestValidateManagementAccessExistingPortButHasName(t *testing.T) {
 		t.Fatal("create callback should not be invoked for existing node")
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(existingNode).Port(existingNodePort)
 	ironic.AddDefaultResponse("/v1/nodes/myns"+nameSeparator+"myhost", "GET", http.StatusNotFound, "")
 	ironic.AddDefaultResponse("/v1/nodes/myhost", "GET", http.StatusNotFound, "")
 	ironic.Start()
@@ -738,7 +738,7 @@ func TestValidateManagementAccessAddTwoHostsWithSameMAC(t *testing.T) {
 		t.Fatal("create callback should not be invoked for existing node")
 	}
 
-	ironic := testserver.NewIronic(t).Ready().CreateNodes(createCallback).Node(existingNode).NodeUpdate(nodes.Node{
+	ironic := testserver.NewIronic(t).CreateNodes(createCallback).Node(existingNode).NodeUpdate(nodes.Node{
 		UUID: "33ce8659-7400-4c68-9535-d10766f07a58",
 	}).Port(existingNodePort)
 	ironic.Start()
@@ -770,7 +770,7 @@ func TestValidateManagementAccessUnsupportedSecureBoot(t *testing.T) {
 	host := makeHost()
 	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
 
-	ironic := testserver.NewIronic(t).Ready().NoNode("myns" + nameSeparator + host.Name).NoNode(host.Name)
+	ironic := testserver.NewIronic(t).NoNode("myns" + nameSeparator + host.Name).NoNode(host.Name)
 	ironic.Start()
 	defer ironic.Stop()
 
@@ -788,7 +788,7 @@ func TestValidateManagementAccessUnsupportedSecureBoot(t *testing.T) {
 }
 
 func TestValidateManagementAccessNoBMCDetails(t *testing.T) {
-	ironic := testserver.NewIronic(t).Ready()
+	ironic := testserver.NewIronic(t)
 	ironic.Start()
 	defer ironic.Stop()
 
@@ -809,7 +809,7 @@ func TestValidateManagementAccessNoBMCDetails(t *testing.T) {
 }
 
 func TestValidateManagementAccessMalformedBMCAddress(t *testing.T) {
-	ironic := testserver.NewIronic(t).Ready()
+	ironic := testserver.NewIronic(t)
 	ironic.Start()
 	defer ironic.Stop()
 
@@ -1160,7 +1160,7 @@ func TestSetExternalURL(t *testing.T) {
 	host := makeHost()
 	host.Spec.BMC.Address = "redfish-virtualmedia://[fe80::fc33:62ff:fe83:8a76]:6233"
 
-	ironic := testserver.NewIronic(t).Ready().
+	ironic := testserver.NewIronic(t).
 		Node(nodes.Node{
 			Name: host.Namespace + nameSeparator + host.Name,
 			UUID: host.Status.Provisioning.ID,
@@ -1190,7 +1190,7 @@ func TestSetExternalURLIPv4(t *testing.T) {
 	host := makeHost()
 	host.Spec.BMC.Address = "redfish-virtualmedia://1.1.1.1:1111"
 
-	ironic := testserver.NewIronic(t).Ready().
+	ironic := testserver.NewIronic(t).
 		Node(nodes.Node{
 			Name: host.Namespace + nameSeparator + host.Name,
 			UUID: host.Status.Provisioning.ID,
@@ -1220,7 +1220,7 @@ func TestSetExternalURLRemoving(t *testing.T) {
 	host := makeHost()
 	host.Spec.BMC.Address = "redfish-virtualmedia://1.1.1.1:1111"
 
-	ironic := testserver.NewIronic(t).Ready().
+	ironic := testserver.NewIronic(t).
 		Node(nodes.Node{
 			Name: host.Namespace + nameSeparator + host.Name,
 			UUID: host.Status.Provisioning.ID,