Repository: metal3-io/baremetal-operator
Pull request #612 (2020-07-31 20:44:25)

keep boot mode updated

metal3-io-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/612#" title="Author self-approved">dhellmann</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/master/OWNERS)~~ [dhellmann]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
dhellmann: /test-integration

dhellmann: /cc @hardys 
zaneb: Looks like a good solution. I wonder if we should move all of `saveHostProvisioningSettings()` into the state machine. Either way this is going to conflict with the RAID patch :(
There's another transition to StateInspecting in `handleExternallyProvisioned()` that we should handle as well.

Patch:
diff --git a/cmd/get-hardware-details/main.go b/cmd/get-hardware-details/main.go
index 3e82eaa4..066adebb 100644
--- a/cmd/get-hardware-details/main.go
+++ b/cmd/get-hardware-details/main.go
@@ -7,27 +7,27 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
 	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
-	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails"
 )
 
-type options struct {
-	Endpoint   string
-	AuthConfig clients.AuthConfig
-	NodeID     string
-}
-
 func main() {
-	opts := getOptions()
+	if len(os.Args) != 3 {
+		fmt.Println("Usage: get-hardware-details <inspector URI> <node UUID>")
+		return
+	}
 
-	inspector, err := clients.InspectorClient(opts.Endpoint, opts.AuthConfig)
+	inspector, err := noauth.NewBareMetalIntrospectionNoAuth(
+		noauth.EndpointOpts{
+			IronicInspectorEndpoint: os.Args[1],
+		})
 	if err != nil {
 		fmt.Printf("could not get inspector client: %s", err)
 		os.Exit(1)
 	}
 
-	introData := introspection.GetIntrospectionData(inspector, opts.NodeID)
+	introData := introspection.GetIntrospectionData(inspector, os.Args[2])
 	data, err := introData.Extract()
 	if err != nil {
 		fmt.Printf("could not get introspection data: %s", err)
@@ -42,19 +42,3 @@ func main() {
 
 	fmt.Println(string(json))
 }
-
-func getOptions() (o options) {
-	if len(os.Args) != 3 {
-		fmt.Println("Usage: get-hardware-details <inspector URI> <node UUID>")
-		os.Exit(1)
-	}
-
-	var err error
-	o.Endpoint, o.AuthConfig, err = clients.ConfigFromEndpointURL(os.Args[1])
-	if err != nil {
-		fmt.Printf("Error: %s\n", err)
-		os.Exit(1)
-	}
-	o.NodeID = os.Args[2]
-	return
-}
diff --git a/docs/ironic-authentication.md b/docs/ironic-authentication.md
deleted file mode 100644
index f7eb6338..00000000
--- a/docs/ironic-authentication.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Authenticating to Ironic
-
-Because hosts under the control of Metal³ need to contact the Ironic and Ironic
-Inspector APIs during inspection and provisioning, it is highly advisable to
-require authentication on those APIs, since the provisioned hosts running user
-workloads will remain connected to the provisioning network.
-
-## Configuration
-
-The `baremetal-operator` supports connecting to Ironic and Ironic Inspector
-configured with the following `auth_strategy` modes:
-
-* `noauth` (no authentication)
-* `http_basic` (HTTP [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication))
-
-Note that Keystone authentication methods are not yet supported.
-
-Authentication configuration is read from the filesystem, beginning at the root
-directory specified in the environment variable `METAL3_AUTH_ROOT_DIR`. If this
-variable is empty or not specified, the default is `/opt/metal3/auth`.
-
-Within the root directory there are separate subdirectories, `ironic` for
-Ironic client configuration, and `ironic-inspector` for Ironic Inspector client
-configuration. (This allows the data to be populated from separate secrets when
-deploying in Kubernetes.)
-
-### `noauth`
-
-This is the default, and will be chosen if the auth root directory does not
-exist. In this mode, the baremetal-operator does not attempt to do any
-authentication against the Ironic APIs.
-
-### `http_basic`
-
-This mode is configured by files in each authentication subdirectory named
-`username` and `password`, and containing the Basic auth username and password,
-respectively.
diff --git a/go.mod b/go.mod
index 8abef41a..e8d69d5b 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/gobuffalo/envy v1.7.1 // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
-	github.com/gophercloud/gophercloud v0.12.0
+	github.com/gophercloud/gophercloud v0.6.0
 	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/imdario/mergo v0.3.8 // indirect
 	github.com/onsi/ginkgo v1.12.0 // indirect
diff --git a/go.sum b/go.sum
index 4bae1625..2b09f1c9 100644
--- a/go.sum
+++ b/go.sum
@@ -410,8 +410,6 @@ github.com/gophercloud/gophercloud v0.2.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEo
 github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gophercloud/gophercloud v0.6.0 h1:Xb2lcqZtml1XjgYZxbeayEemq7ASbeTp09m36gQFpEU=
 github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM=
-github.com/gophercloud/gophercloud v0.12.0 h1:mZrie07npp6ODiwHZolTicr5jV8Ogn43AvAsSMm6Ork=
-github.com/gophercloud/gophercloud v0.12.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -899,7 +897,6 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
@@ -966,7 +963,6 @@ golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1027,7 +1023,6 @@ golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
@@ -1085,7 +1080,6 @@ golang.org/x/tools v0.0.0-20191111182352-50fa39b762bc/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200327195553-82bb89366a1e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200430192856-2840dafb9ee1 h1:OlmCHPqCyX+EpIpxG55cfMJuINAFd7HHTdWwA3yyelQ=
diff --git a/pkg/controller/baremetalhost/baremetalhost_controller.go b/pkg/controller/baremetalhost/baremetalhost_controller.go
index 8eaa2c8e..69ab7e2f 100644
--- a/pkg/controller/baremetalhost/baremetalhost_controller.go
+++ b/pkg/controller/baremetalhost/baremetalhost_controller.go
@@ -487,14 +487,6 @@ func (r *ReconcileBareMetalHost) actionRegistering(prov provisioner.Provisioner,
 		info.postSaveCallbacks = append(info.postSaveCallbacks, updatedCredentials.Inc)
 	}
 
-	// Make sure we have saved the current boot mode value.
-	bootMode := info.host.BootMode()
-	if bootMode != info.host.Status.Provisioning.BootMode {
-		info.log.Info("saving boot mode")
-		info.host.Status.Provisioning.BootMode = bootMode
-		return actionContinue{}
-	}
-
 	provResult, err := prov.ValidateManagementAccess(credsChanged)
 	if err != nil {
 		noManagementAccess.Inc()
diff --git a/pkg/controller/baremetalhost/host_state_machine.go b/pkg/controller/baremetalhost/host_state_machine.go
index 06507193..8d6e9140 100644
--- a/pkg/controller/baremetalhost/host_state_machine.go
+++ b/pkg/controller/baremetalhost/host_state_machine.go
@@ -114,6 +114,16 @@ func (hsm *hostStateMachine) ReconcileState(info *reconcileInfo) actionResult {
 	return actionError{fmt.Errorf("No handler found for state \"%s\"", initialState)}
 }
 
+func updateBootModeStatus(host *metal3v1alpha1.BareMetalHost) bool {
+	// Make sure we have saved the current boot mode value.
+	bootMode := host.BootMode()
+	if bootMode == host.Status.Provisioning.BootMode {
+		return false
+	}
+	host.Status.Provisioning.BootMode = bootMode
+	return true
+}
+
 func (hsm *hostStateMachine) checkInitiateDelete() bool {
 	if hsm.Host.DeletionTimestamp.IsZero() {
 		// Delete not requested
@@ -189,6 +199,9 @@ func (hsm *hostStateMachine) handleRegistering(info *reconcileInfo) actionResult
 			hsm.NextState = metal3v1alpha1.StateProvisioned
 		case hsm.Host.NeedsHardwareInspection():
 			hsm.NextState = metal3v1alpha1.StateInspecting
+			if updateBootModeStatus(hsm.Host) {
+				info.log.Info("saving boot mode")
+			}
 		case hsm.Host.NeedsHardwareProfile():
 			hsm.NextState = metal3v1alpha1.StateMatchProfile
 		default:
@@ -242,6 +255,9 @@ func (hsm *hostStateMachine) handleExternallyProvisioned(info *reconcileInfo) ac
 	switch {
 	case hsm.Host.NeedsHardwareInspection():
 		hsm.NextState = metal3v1alpha1.StateInspecting
+		if updateBootModeStatus(hsm.Host) {
+			info.log.Info("saving boot mode")
+		}
 	case hsm.Host.NeedsHardwareProfile():
 		hsm.NextState = metal3v1alpha1.StateMatchProfile
 	default:
@@ -261,6 +277,9 @@ func (hsm *hostStateMachine) handleReady(info *reconcileInfo) actionResult {
 	switch r := actResult.(type) {
 	case actionComplete:
 		hsm.NextState = metal3v1alpha1.StateProvisioning
+		if updateBootModeStatus(hsm.Host) {
+			info.log.Info("saving boot mode")
+		}
 	case actionFailed:
 		switch r.ErrorType {
 		case metal3v1alpha1.PowerManagementError:
diff --git a/pkg/provisioner/ironic/clients/auth.go b/pkg/provisioner/ironic/clients/auth.go
deleted file mode 100644
index 55a3340e..00000000
--- a/pkg/provisioner/ironic/clients/auth.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package clients
-
-import (
-	"fmt"
-	"io/ioutil"
-	"net/url"
-	"os"
-	"path"
-	"path/filepath"
-	"strings"
-)
-
-// AuthType is the method of authenticating requests to the server
-type AuthType string
-
-const (
-	// NoAuth uses no authentication
-	NoAuth AuthType = "noauth"
-	// HTTPBasicAuth uses HTTP Basic Authentication
-	HTTPBasicAuth AuthType = "http_basic"
-)
-
-// AuthConfig contains data needed to configure authentication in the client
-type AuthConfig struct {
-	Type     AuthType
-	Username string
-	Password string
-}
-
-func authRoot() string {
-	env := os.Getenv("METAL3_AUTH_ROOT_DIR")
-	if env != "" {
-		return filepath.Clean(env)
-	}
-	return "/opt/metal3/auth"
-}
-
-func readAuthFile(filename string) (string, error) {
-	content, err := ioutil.ReadFile(filepath.Clean(filename))
-	return strings.TrimSpace(string(content)), err
-}
-
-func load(clientType string) (auth AuthConfig, err error) {
-	authPath := path.Join(authRoot(), clientType)
-
-	if _, err := os.Stat(authPath); err != nil {
-		if os.IsNotExist(err) {
-			auth.Type = NoAuth
-			return auth, nil
-		}
-		return auth, err
-	}
-	auth.Type = HTTPBasicAuth
-
-	auth.Username, err = readAuthFile(path.Join(authPath, "username"))
-	if err != nil {
-		return
-	}
-
-	auth.Password, err = readAuthFile(path.Join(authPath, "password"))
-	if err != nil {
-		return
-	}
-
-	if auth.Username == "" {
-		err = fmt.Errorf("Empty HTTP Basic Auth username")
-	} else if auth.Password == "" {
-		err = fmt.Errorf("Empty HTTP Basic Auth password")
-	}
-	return
-}
-
-// LoadAuth loads the Ironic and Inspector configuration from the environment
-func LoadAuth() (ironicAuth, inspectorAuth AuthConfig, err error) {
-	ironicAuth, err = load("ironic")
-	if err != nil {
-		return
-	}
-	inspectorAuth, err = load("ironic-inspector")
-	return
-}
-
-// ConfigFromEndpointURL returns an endpoint and an auth config from an
-// endpoint URL that may contain HTTP basic auth credentials.
-func ConfigFromEndpointURL(endpointURL string) (endpoint string, auth AuthConfig, err error) {
-	parsedURL, err := url.Parse(endpointURL)
-	if err != nil {
-		return
-	}
-
-	if parsedURL.User != nil {
-		var hasPasswd bool
-		auth.Type = HTTPBasicAuth
-		auth.Username = parsedURL.User.Username()
-		auth.Password, hasPasswd = parsedURL.User.Password()
-		if !hasPasswd {
-			err = fmt.Errorf("No password supplied for HTTP Basic Auth")
-		}
-		parsedURL.User = nil
-	} else {
-		auth.Type = NoAuth
-	}
-
-	endpoint = parsedURL.String()
-	return
-}
diff --git a/pkg/provisioner/ironic/clients/client.go b/pkg/provisioner/ironic/clients/client.go
deleted file mode 100644
index 398086b3..00000000
--- a/pkg/provisioner/ironic/clients/client.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package clients
-
-import (
-	"fmt"
-
-	"github.com/gophercloud/gophercloud"
-	"github.com/gophercloud/gophercloud/openstack/baremetal/httpbasic"
-	"github.com/gophercloud/gophercloud/openstack/baremetal/noauth"
-	httpbasicintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/httpbasic"
-	noauthintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
-)
-
-// IronicClient creates a client for Ironic
-func IronicClient(ironicEndpoint string, auth AuthConfig) (client *gophercloud.ServiceClient, err error) {
-	switch auth.Type {
-	case NoAuth:
-		client, err = noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
-			IronicEndpoint: ironicEndpoint,
-		})
-	case HTTPBasicAuth:
-		client, err = httpbasic.NewBareMetalHTTPBasic(httpbasic.EndpointOpts{
-			IronicEndpoint:     ironicEndpoint,
-			IronicUser:         auth.Username,
-			IronicUserPassword: auth.Password,
-		})
-	default:
-		err = fmt.Errorf("Unknown auth type %s", auth.Type)
-	}
-	return
-}
-
-// InspectorClient creates a client for Ironic Inspector
-func InspectorClient(inspectorEndpoint string, auth AuthConfig) (client *gophercloud.ServiceClient, err error) {
-	switch auth.Type {
-	case NoAuth:
-		client, err = noauthintrospection.NewBareMetalIntrospectionNoAuth(
-			noauthintrospection.EndpointOpts{
-				IronicInspectorEndpoint: inspectorEndpoint,
-			})
-	case HTTPBasicAuth:
-		client, err = httpbasicintrospection.NewBareMetalIntrospectionHTTPBasic(httpbasicintrospection.EndpointOpts{
-			IronicInspectorEndpoint:     inspectorEndpoint,
-			IronicInspectorUser:         auth.Username,
-			IronicInspectorUserPassword: auth.Password,
-		})
-	default:
-		err = fmt.Errorf("Unknown auth type %s", auth.Type)
-	}
-	return
-}
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 21d034f5..8c69d2e5 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -9,8 +9,10 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/baremetal/noauth"
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
+	noauthintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
 	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
 
 	"github.com/pkg/errors"
@@ -22,7 +24,6 @@ import (
 	"github.com/metal3-io/baremetal-operator/pkg/bmc"
 	"github.com/metal3-io/baremetal-operator/pkg/hardware"
 	"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"
 )
@@ -37,8 +38,6 @@ var deployKernelURL string
 var deployRamdiskURL string
 var ironicEndpoint string
 var inspectorEndpoint string
-var ironicAuth clients.AuthConfig
-var inspectorAuth clients.AuthConfig
 
 const (
 	// See nodes.Node.PowerState for details
@@ -56,14 +55,6 @@ var bootModeCapabilities = map[metal3v1alpha1.BootMode]string{
 func init() {
 	// NOTE(dhellmann): Use Fprintf() to report errors instead of
 	// logging, because logging is not configured yet in init().
-
-	var authErr error
-	ironicAuth, inspectorAuth, authErr = clients.LoadAuth()
-	if authErr != nil {
-		fmt.Fprintf(os.Stderr, "Cannot start: %s\n", authErr)
-		os.Exit(1)
-	}
-
 	deployKernelURL = os.Getenv("DEPLOY_KERNEL_URL")
 	if deployKernelURL == "" {
 		fmt.Fprintf(os.Stderr, "Cannot start: No DEPLOY_KERNEL_URL variable set\n")
@@ -112,9 +103,7 @@ type ironicProvisioner struct {
 func LogStartup() {
 	log.Info("ironic settings",
 		"endpoint", ironicEndpoint,
-		"ironicAuthType", ironicAuth.Type,
 		"inspectorEndpoint", inspectorEndpoint,
-		"inspectorAuthType", inspectorAuth.Type,
 		"deployKernelURL", deployKernelURL,
 		"deployRamdiskURL", deployRamdiskURL,
 	)
@@ -123,35 +112,36 @@ func LogStartup() {
 // A private function to construct an ironicProvisioner (rather than a
 // Provisioner interface) in a consistent way for tests.
 func newProvisioner(host *metal3v1alpha1.BareMetalHost, bmcCreds bmc.Credentials, publisher provisioner.EventPublisher) (*ironicProvisioner, error) {
-	clientIronic, err := clients.IronicClient(ironicEndpoint, ironicAuth)
-	if err != nil {
-		return nil, err
-	}
-
-	clientInspector, err := clients.InspectorClient(inspectorEndpoint, inspectorAuth)
+	client, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
+		IronicEndpoint: ironicEndpoint,
+	})
 	if err != nil {
 		return nil, err
 	}
-
 	bmcAccess, err := bmc.NewAccessDetails(host.Spec.BMC.Address, host.Spec.BMC.DisableCertificateVerification)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse BMC address information")
 	}
-
+	inspector, err := noauthintrospection.NewBareMetalIntrospectionNoAuth(
+		noauthintrospection.EndpointOpts{
+			IronicInspectorEndpoint: inspectorEndpoint,
+		})
+	if err != nil {
+		return nil, err
+	}
 	// Ensure we have a microversion high enough to get the features
 	// we need.
-	clientIronic.Microversion = "1.56"
+	client.Microversion = "1.56"
 	p := &ironicProvisioner{
 		host:      host,
 		status:    &(host.Status.Provisioning),
 		bmcAccess: bmcAccess,
 		bmcCreds:  bmcCreds,
-		client:    clientIronic,
-		inspector: clientInspector,
+		client:    client,
+		inspector: inspector,
 		log:       log.WithValues("host", host.Name),
 		publisher: publisher,
 	}
-
 	return p, nil
 }
 
@@ -253,8 +243,6 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 	if ironicNode == nil {
 		p.log.Info("registering host in ironic")
 
-		bootMode := p.host.BootMode()
-
 		ironicNode, err = nodes.Create(
 			p.client,
 			nodes.CreateOpts{
@@ -268,7 +256,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 				RAIDInterface:       p.bmcAccess.RAIDInterface(),
 				VendorInterface:     p.bmcAccess.VendorInterface(),
 				Properties: map[string]interface{}{
-					"capabilities": bootModeCapabilities[bootMode],
+					"capabilities": bootModeCapabilities[p.host.Status.Provisioning.BootMode],
 				},
 			}).Extract()
 		// FIXME(dhellmann): Handle 409 and 503? errors here.
@@ -805,9 +793,69 @@ func (p *ironicProvisioner) getUpdateOptsForNode(ironicNode *nodes.Node) (update
 		},
 	)
 
+	// boot_mode
+	op, value := buildCapabilitiesValue(ironicNode, p.host.Status.Provisioning.BootMode)
+	updates = append(
+		updates,
+		nodes.UpdateOperation{
+			Op:    op,
+			Path:  "/properties/capabilities",
+			Value: value,
+		},
+	)
+
 	return updates, nil
 }
 
+// We can't just replace the capabilities because we need to keep the
+// values provided by inspection. We can't replace only the boot_mode
+// because the API isn't fine-grained enough for that. So we have to
+// look at the existing value and modify it. This function
+// encapsulates the logic for building the value and knowing which
+// update operation to use with the results.
+func buildCapabilitiesValue(ironicNode *nodes.Node, bootMode metal3v1alpha1.BootMode) (op nodes.UpdateOp, value string) {
+
+	capabilities, ok := ironicNode.Properties["capabilities"]
+	if !ok {
+		// There is no existing capabilities value
+		return nodes.AddOp, bootModeCapabilities[bootMode]
+	}
+	existingCapabilities := capabilities.(string)
+
+	// The capabilities value is set, so we will want to replace it.
+	op = nodes.ReplaceOp
+
+	if existingCapabilities == "" {
+		// The existing value is empty so we can replace the whole
+		// thing.
+		value = bootModeCapabilities[bootMode]
+		return
+	}
+
+	if !strings.Contains(existingCapabilities, "boot_mode") {
+		// No boot_mode is set, append and return
+		value = fmt.Sprintf("%s,%s", existingCapabilities, bootModeCapabilities[bootMode])
+		return
+	}
+
+	// The capabilities value has format "var1:val1,var2:val2". We
+	// know that boot_mode is there but not what value it has.  There
+	// are only 2 values, so we can replace the "wrong" string with
+	// the right one without fully parsing the string. We may want to
+	// change this later when we have more boot modes.
+
+	var fromMode metal3v1alpha1.BootMode
+	if bootMode == metal3v1alpha1.UEFI {
+		fromMode = metal3v1alpha1.Legacy
+	} else {
+		fromMode = metal3v1alpha1.UEFI
+	}
+	value = strings.ReplaceAll(existingCapabilities,
+		bootModeCapabilities[fromMode], bootModeCapabilities[bootMode])
+
+	return
+}
+
 func (p *ironicProvisioner) startProvisioning(ironicNode *nodes.Node, hostConf provisioner.HostConfigData) (result provisioner.Result, err error) {
 
 	p.log.Info("starting provisioning", "node properties", ironicNode.Properties)


Test patch:
diff --git a/pkg/provisioner/ironic/clients/auth_test.go b/pkg/provisioner/ironic/clients/auth_test.go
deleted file mode 100644
index a304c138..00000000
--- a/pkg/provisioner/ironic/clients/auth_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package clients
-
-import (
-	"testing"
-)
-
-func TestConfigFromEndpointURL(t *testing.T) {
-	testCases := []struct {
-		Scenario         string
-		URL              string
-		ExpectedEndpoint string
-		ExpectedAuth     AuthConfig
-		ExpectErr        bool
-	}{
-		{
-			Scenario:  "garbage",
-			URL:       "@foo::bar",
-			ExpectErr: true,
-		},
-		{
-			Scenario:         "no auth",
-			URL:              "https://example.test/v1",
-			ExpectedEndpoint: "https://example.test/v1",
-			ExpectedAuth: AuthConfig{
-				Type: NoAuth,
-			},
-		},
-		{
-			Scenario:         "username-only",
-			URL:              "https://user@example.test/v1",
-			ExpectedEndpoint: "https://example.test/v1",
-			ExpectedAuth: AuthConfig{
-				Type:     HTTPBasicAuth,
-				Username: "user",
-			},
-			ExpectErr: true,
-		},
-		{
-			Scenario:         "empty password",
-			URL:              "https://user:@example.test/v1",
-			ExpectedEndpoint: "https://example.test/v1",
-			ExpectedAuth: AuthConfig{
-				Type:     HTTPBasicAuth,
-				Username: "user",
-				Password: "",
-			},
-		},
-		{
-			Scenario:         "basic auth",
-			URL:              "https://user:pass@example.test/v1",
-			ExpectedEndpoint: "https://example.test/v1",
-			ExpectedAuth: AuthConfig{
-				Type:     HTTPBasicAuth,
-				Username: "user",
-				Password: "pass",
-			},
-		},
-		{
-			Scenario:         "IPv6 no auth",
-			URL:              "https://[::1]/v1",
-			ExpectedEndpoint: "https://[::1]/v1",
-			ExpectedAuth: AuthConfig{
-				Type: NoAuth,
-			},
-		},
-		{
-			Scenario:         "IPv6 basic auth",
-			URL:              "https://user:pass@[::1]/v1",
-			ExpectedEndpoint: "https://[::1]/v1",
-			ExpectedAuth: AuthConfig{
-				Type:     HTTPBasicAuth,
-				Username: "user",
-				Password: "pass",
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.Scenario, func(t *testing.T) {
-			ep, auth, err := ConfigFromEndpointURL(tc.URL)
-			if ep != tc.ExpectedEndpoint {
-				t.Errorf("Expected endpoint '%s', got '%s'",
-					tc.ExpectedEndpoint, ep)
-			}
-			if auth.Type != tc.ExpectedAuth.Type {
-				t.Errorf("Unexpected auth type %s", auth.Type)
-			}
-			if auth.Username != tc.ExpectedAuth.Username {
-				t.Errorf("Unexpected username '%s'", auth.Username)
-			}
-			if auth.Password != tc.ExpectedAuth.Password {
-				t.Errorf("Unexpected password '%s'", auth.Password)
-			}
-			if (err != nil) != tc.ExpectErr {
-				t.Errorf("Unexpected error %s", err)
-			}
-		})
-	}
-}
diff --git a/pkg/provisioner/ironic/ironic_test.go b/pkg/provisioner/ironic/ironic_test.go
index edd5c2db..1607ae57 100644
--- a/pkg/provisioner/ironic/ironic_test.go
+++ b/pkg/provisioner/ironic/ironic_test.go
@@ -501,3 +501,96 @@ func (m *mockServer) start() *mockServer {
 func (m *mockServer) stop() {
 	m.server.Close()
 }
+
+func TestBuildCapabilitiesValue(t *testing.T) {
+
+	cases := []struct {
+		Scenario      string
+		Node          nodes.Node
+		Mode          metal3v1alpha1.BootMode
+		ExpectedValue string
+		ExpectedOp    nodes.UpdateOp
+	}{
+		{
+			Scenario:      "unset",
+			Node:          nodes.Node{},
+			Mode:          metal3v1alpha1.UEFI,
+			ExpectedValue: "boot_mode:uefi",
+			ExpectedOp:    nodes.AddOp,
+		},
+		{
+			Scenario: "empty",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "",
+				},
+			},
+			Mode:          metal3v1alpha1.UEFI,
+			ExpectedValue: "boot_mode:uefi",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+		{
+			Scenario: "not-there",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+				},
+			},
+			Mode:          metal3v1alpha1.UEFI,
+			ExpectedValue: "cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true,boot_mode:uefi",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+		{
+			Scenario: "uefi-to-uefi",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "boot_mode:uefi,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+				},
+			},
+			Mode:          metal3v1alpha1.UEFI,
+			ExpectedValue: "boot_mode:uefi,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+		{
+			Scenario: "bios-to-bios",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "boot_mode:bios,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+				},
+			},
+			Mode:          metal3v1alpha1.Legacy,
+			ExpectedValue: "boot_mode:bios,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+		{
+			Scenario: "bios-to-uefi",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "boot_mode:bios,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+				},
+			},
+			Mode:          metal3v1alpha1.UEFI,
+			ExpectedValue: "boot_mode:uefi,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+		{
+			Scenario: "uefi-to-bios",
+			Node: nodes.Node{
+				Properties: map[string]interface{}{
+					"capabilities": "boot_mode:uefi,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+				},
+			},
+			Mode:          metal3v1alpha1.Legacy,
+			ExpectedValue: "boot_mode:bios,cpu_vt:true,cpu_aes:true,cpu_hugepages:true,cpu_hugepages_1g:true",
+			ExpectedOp:    nodes.ReplaceOp,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.Scenario, func(t *testing.T) {
+			actualOp, actualVal := buildCapabilitiesValue(&tc.Node, tc.Mode)
+			assert.Equal(t, tc.ExpectedOp, actualOp)
+			assert.Equal(t, tc.ExpectedValue, actualVal)
+		})
+	}
+}