diff --git a/apis/metal3.io/v1alpha1/baremetalhost_types.go b/apis/metal3.io/v1alpha1/baremetalhost_types.go index 75c7c2bd26..13ccf32549 100644 --- a/apis/metal3.io/v1alpha1/baremetalhost_types.go +++ b/apis/metal3.io/v1alpha1/baremetalhost_types.go @@ -135,6 +135,9 @@ const ( // InspectionError is an error condition occurring when an attempt to // obtain hardware details from the Host fails. InspectionError ErrorType = "inspection error" + // PreparationError is an error condition occurring when do + // cleaning steps failed. + PreparationError ErrorType = "preparation error" // ProvisioningError is an error condition occuring when the controller // fails to provision or deprovision the Host. ProvisioningError ErrorType = "provisioning error" @@ -162,6 +165,9 @@ const ( // against known hardware profiles StateMatchProfile ProvisioningState = "match profile" + // StatePreparing means we are removing existing configuration and set new configuration to the host + StatePreparing ProvisioningState = "preparing" + // StateReady means the host can be consumed StateReady ProvisioningState = "ready" @@ -530,7 +536,7 @@ type BareMetalHostStatus struct { // ErrorType indicates the type of failure encountered when the // OperationalStatus is OperationalStatusError - // +kubebuilder:validation:Enum=provisioned registration error;registration error;inspection error;provisioning error;power management error + // +kubebuilder:validation:Enum=provisioned registration error;registration error;inspection error;preparation error;provisioning error;power management error ErrorType ErrorType `json:"errorType,omitempty"` // LastUpdated identifies when this status was last observed. diff --git a/config/crd/bases/metal3.io_baremetalhosts.yaml b/config/crd/bases/metal3.io_baremetalhosts.yaml index 8f1a821997..644468ee41 100644 --- a/config/crd/bases/metal3.io_baremetalhosts.yaml +++ b/config/crd/bases/metal3.io_baremetalhosts.yaml @@ -265,6 +265,7 @@ spec: - provisioned registration error - registration error - inspection error + - preparation error - provisioning error - power management error type: string diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml index 58b3842f09..7f2576b14b 100644 --- a/config/render/capm3.yaml +++ b/config/render/capm3.yaml @@ -263,6 +263,7 @@ spec: - provisioned registration error - registration error - inspection error + - preparation error - provisioning error - power management error type: string diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go index 67b0b9e640..9e83904e3d 100644 --- a/controllers/metal3.io/baremetalhost_controller.go +++ b/controllers/metal3.io/baremetalhost_controller.go @@ -455,6 +455,9 @@ func (r *BareMetalHostReconciler) registerHost(prov provisioner.Provisioner, inf if provID != "" && info.host.Status.Provisioning.ID != provID { info.log.Info("setting provisioning id", "ID", provID) info.host.Status.Provisioning.ID = provID + if info.host.Status.Provisioning.State == metal3v1alpha1.StatePreparing { + clearHostProvisioningSettings(info.host) + } dirty = true } @@ -564,6 +567,44 @@ func (r *BareMetalHostReconciler) actionMatchProfile(prov provisioner.Provisione return actionComplete{} } +func (r *BareMetalHostReconciler) actionPreparing(prov provisioner.Provisioner, info *reconcileInfo) actionResult { + info.log.Info("preparing") + + // Save provisioning settings. + provisioningSettings := info.host.Status.Provisioning.DeepCopy() + dirty, err := saveHostProvisioningSettings(info.host) + if err != nil { + return actionError{errors.Wrap(err, "Could not save the host provisioning settings")} + } + + // Do prepare(manual clean). + provResult, started, err := prov.Prepare(dirty) + if err != nil { + return actionError{errors.Wrap(err, "error preparing host")} + } + + if provResult.ErrorMessage != "" { + info.log.Info("handling cleaning error in controller") + clearHostProvisioningSettings(info.host) + return recordActionFailure(info, metal3v1alpha1.PreparationError, provResult.ErrorMessage) + } + + if provResult.Dirty { + result := actionContinue{provResult.RequeueAfter} + if clearError(info.host) || (dirty && started) { + // If clearError return true, but started is false, restore provisioningSettings. + if dirty && !started { + info.host.Status.Provisioning = *provisioningSettings + } + return actionUpdate{result} + } + return result + } + + clearError(info.host) + return actionComplete{} +} + // Start/continue provisioning if we need to. func (r *BareMetalHostReconciler) actionProvisioning(prov provisioner.Provisioner, info *reconcileInfo) actionResult { hostConf := &hostConfigData{ @@ -796,7 +837,9 @@ func (r *BareMetalHostReconciler) actionManageReady(prov provisioner.Provisioner return actionError{errors.Wrap(err, "Could not save the host provisioning settings")} } if dirty { - info.log.Info("updating host provisioning settings") + info.log.Info("Host provisioning settings have been updated, go back to Preparing state") + clearHostProvisioningSettings(info.host) + return actionUpdate{} } clearError(info.host) return actionComplete{} diff --git a/controllers/metal3.io/baremetalhost_controller_test.go b/controllers/metal3.io/baremetalhost_controller_test.go index 4cff51bf8a..cf868410ad 100644 --- a/controllers/metal3.io/baremetalhost_controller_test.go +++ b/controllers/metal3.io/baremetalhost_controller_test.go @@ -923,12 +923,12 @@ func TestExternallyProvisionedTransitions(t *testing.T) { waitForProvisioningState(t, r, host, metal3v1alpha1.StateInspecting) }) - t.Run("ready to externally provisioned", func(t *testing.T) { + t.Run("preparing to externally provisioned", func(t *testing.T) { host := newDefaultHost(t) host.Spec.Online = true r := newTestReconciler(host) - waitForProvisioningState(t, r, host, metal3v1alpha1.StateReady) + waitForProvisioningState(t, r, host, metal3v1alpha1.StatePreparing) host.Spec.ExternallyProvisioned = true err := r.Update(goctx.TODO(), host) diff --git a/controllers/metal3.io/demo_test.go b/controllers/metal3.io/demo_test.go index 0560323fd9..55aec01e8e 100644 --- a/controllers/metal3.io/demo_test.go +++ b/controllers/metal3.io/demo_test.go @@ -86,6 +86,48 @@ func TestDemoInspecting(t *testing.T) { ) } +func TestDemoPreparing(t *testing.T) { + host := newDefaultNamedHost(demo.PreparingHost, t) + host.Spec.Image = &metal3v1alpha1.Image{ + URL: "a-url", + Checksum: "a-checksum", + } + host.Spec.Online = true + r := newDemoReconciler(host) + + tryReconcile(t, r, host, + func(host *metal3v1alpha1.BareMetalHost, result reconcile.Result) bool { + t.Logf("Status: %q State: %q ErrorMessage: %q", + host.OperationalStatus(), + host.Status.Provisioning.State, + host.Status.ErrorMessage, + ) + return host.Status.Provisioning.State == metal3v1alpha1.StatePreparing + }, + ) +} + +func TestDemoPreparingError(t *testing.T) { + host := newDefaultNamedHost(demo.PreparingErrorHost, t) + host.Spec.Image = &metal3v1alpha1.Image{ + URL: "a-url", + Checksum: "a-checksum", + } + host.Spec.Online = true + r := newDemoReconciler(host) + + tryReconcile(t, r, host, + func(host *metal3v1alpha1.BareMetalHost, result reconcile.Result) bool { + t.Logf("Status: %q State: %q ErrorMessage: %q", + host.OperationalStatus(), + host.Status.Provisioning.State, + host.Status.ErrorMessage, + ) + return host.Status.Provisioning.State == metal3v1alpha1.StatePreparing + }, + ) +} + // TestDemoReady tests that a host with the right name reports // that it is ready to be provisioned func TestDemoReady(t *testing.T) { diff --git a/controllers/metal3.io/host_state_machine.go b/controllers/metal3.io/host_state_machine.go index 4f9a1c77f0..3eac09426d 100644 --- a/controllers/metal3.io/host_state_machine.go +++ b/controllers/metal3.io/host_state_machine.go @@ -46,6 +46,7 @@ func (hsm *hostStateMachine) handlers() map[metal3v1alpha1.ProvisioningState]sta metal3v1alpha1.StateExternallyProvisioned: hsm.handleExternallyProvisioned, metal3v1alpha1.StateMatchProfile: hsm.handleMatchProfile, metal3v1alpha1.StateAvailable: hsm.handleReady, + metal3v1alpha1.StatePreparing: hsm.handlePreparing, metal3v1alpha1.StateReady: hsm.handleReady, metal3v1alpha1.StateProvisioning: hsm.handleProvisioning, metal3v1alpha1.StateProvisioned: hsm.handleProvisioned, @@ -311,7 +312,7 @@ func (hsm *hostStateMachine) handleInspecting(info *reconcileInfo) actionResult func (hsm *hostStateMachine) handleMatchProfile(info *reconcileInfo) actionResult { actResult := hsm.Reconciler.actionMatchProfile(hsm.Provisioner, info) if _, complete := actResult.(actionComplete); complete { - hsm.NextState = metal3v1alpha1.StateReady + hsm.NextState = metal3v1alpha1.StatePreparing hsm.Host.Status.ErrorCount = 0 } return actResult @@ -329,20 +330,32 @@ func (hsm *hostStateMachine) handleExternallyProvisioned(info *reconcileInfo) ac case hsm.Host.NeedsHardwareProfile(): hsm.NextState = metal3v1alpha1.StateMatchProfile default: - hsm.NextState = metal3v1alpha1.StateReady + hsm.NextState = metal3v1alpha1.StatePreparing } return actionComplete{} } +func (hsm *hostStateMachine) handlePreparing(info *reconcileInfo) actionResult { + actResult := hsm.Reconciler.actionPreparing(hsm.Provisioner, info) + if _, complete := actResult.(actionComplete); complete { + hsm.Host.Status.ErrorCount = 0 + hsm.NextState = metal3v1alpha1.StateReady + } + return actResult +} + func (hsm *hostStateMachine) handleReady(info *reconcileInfo) actionResult { if hsm.Host.Spec.ExternallyProvisioned { hsm.NextState = metal3v1alpha1.StateExternallyProvisioned + clearHostProvisioningSettings(info.host) return actionComplete{} } // ErrorCount is cleared when appropriate inside actionManageReady actResult := hsm.Reconciler.actionManageReady(hsm.Provisioner, info) - if _, complete := actResult.(actionComplete); complete { + if _, update := actResult.(actionUpdate); update { + hsm.NextState = metal3v1alpha1.StatePreparing + } else if _, complete := actResult.(actionComplete); complete { hsm.NextState = metal3v1alpha1.StateProvisioning } return actResult diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go index 39e60d80c9..162cf68947 100644 --- a/controllers/metal3.io/host_state_machine_test.go +++ b/controllers/metal3.io/host_state_machine_test.go @@ -45,7 +45,7 @@ func TestProvisioningCapacity(t *testing.T) { }, { Scenario: "transition-to-provisioning-delayed", - Host: host(metal3v1alpha1.StateReady).build(), + Host: host(metal3v1alpha1.StateReady).SaveHostProvisioningSettings().build(), HasProvisioningCapacity: false, ExpectedProvisioningState: metal3v1alpha1.StateReady, @@ -61,7 +61,7 @@ func TestProvisioningCapacity(t *testing.T) { }, { Scenario: "transition-to-provisioning-ok", - Host: host(metal3v1alpha1.StateReady).build(), + Host: host(metal3v1alpha1.StateReady).SaveHostProvisioningSettings().build(), HasProvisioningCapacity: true, ExpectedProvisioningState: metal3v1alpha1.StateProvisioning, @@ -379,8 +379,13 @@ func TestErrorCountClearedOnStateTransition(t *testing.T) { TargetState: metal3v1alpha1.StateMatchProfile, }, { - Scenario: "matchprofile-to-ready", + Scenario: "matchprofile-to-preparing", Host: host(metal3v1alpha1.StateMatchProfile).build(), + TargetState: metal3v1alpha1.StatePreparing, + }, + { + Scenario: "preparing-to-ready", + Host: host(metal3v1alpha1.StatePreparing).build(), TargetState: metal3v1alpha1.StateReady, }, { @@ -504,6 +509,11 @@ func (hb *hostBuilder) build() *metal3v1alpha1.BareMetalHost { return &hb.BareMetalHost } +func (hb *hostBuilder) SaveHostProvisioningSettings() *hostBuilder { + saveHostProvisioningSettings(&hb.BareMetalHost) + return hb +} + func (hb *hostBuilder) SetTriedCredentials() *hostBuilder { hb.Status.TriedCredentials = hb.Status.GoodCredentials return hb @@ -617,6 +627,10 @@ func (m *mockProvisioner) UpdateHardwareState() (hwState provisioner.HardwareSta return } +func (m *mockProvisioner) Prepare(unprepared bool) (result provisioner.Result, started bool, err error) { + return m.getNextResultByMethod("Prepare"), m.nextResults["Prepare"].Dirty, err +} + func (m *mockProvisioner) Adopt(force bool) (result provisioner.Result, err error) { return m.getNextResultByMethod("Adopt"), err } diff --git a/docs/BaremetalHost_ProvisioningState.dot b/docs/BaremetalHost_ProvisioningState.dot index 62e9335529..cd9412bb1d 100644 --- a/docs/BaremetalHost_ProvisioningState.dot +++ b/docs/BaremetalHost_ProvisioningState.dot @@ -19,7 +19,7 @@ digraph BaremetalHost { ExternallyProvisioned -> Inspecting [label="!externallyProvisioned && NeedsHardwareInspection()"] ExternallyProvisioned -> MatchProfile [label="!externallyProvisioned && NeedsHardwareProfile()"] - ExternallyProvisioned -> Ready [label="!externallyProvisioned"] + ExternallyProvisioned -> Preparing [label="!externallyProvisioned"] Ready -> ExternallyProvisioned [label="externallyProvisioned"] Inspecting -> MatchProfile [label="done"] @@ -27,18 +27,24 @@ digraph BaremetalHost { Deleting3 [shape=point] - MatchProfile -> Ready [label="done"] + MatchProfile -> Preparing [label="done"] MatchProfile -> Deleting4 [label="!DeletionTimestamp.IsZero()"] Deleting4 [shape=point] Deleting5 [shape=point] + Preparing -> Ready [label="done"] + Preparing -> Deleting6 [label="!DeletionTimestamp.IsZero()"] + + Deleting6 [shape=point] + Ready [shape=doublecircle] Ready -> Provisioning [label="NeedsProvisioning()"] - Ready -> Deleting6 [label="!DeletionTimestamp.IsZero()"] + Ready -> Preparing [label="saveHostProvisioningSettings()"] + Ready -> Deleting7 [label="!DeletionTimestamp.IsZero()"] - Deleting6 [shape=point] + Deleting7 [shape=point] Provisioning -> Provisioned [label=done] Provisioning -> Deprovisioning [label="failed || !DeletionTimestamp.IsZero()"] diff --git a/docs/BaremetalHost_ProvisioningState.png b/docs/BaremetalHost_ProvisioningState.png index 1a6020e199..437a9209f6 100644 Binary files a/docs/BaremetalHost_ProvisioningState.png and b/docs/BaremetalHost_ProvisioningState.png differ diff --git a/docs/baremetalhost-states.md b/docs/baremetalhost-states.md index 9cffc2b154..f494b6858e 100644 --- a/docs/baremetalhost-states.md +++ b/docs/baremetalhost-states.md @@ -41,6 +41,12 @@ will stay in the Inspecting state until this process is completed. A host in the Match Profile state is being matched against a hardware profile. +## Preparing + +When setting up RAID, BIOS and other similar configurations, +the host will be in Preparing state. For ironic provisioner, +we build and set up manual clean steps in Preparing state. + ## Ready A host in the Ready state is available to be provisioned. diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go index 3d336fa371..44477802c5 100644 --- a/pkg/provisioner/demo/demo.go +++ b/pkg/provisioner/demo/demo.go @@ -30,6 +30,12 @@ const ( // InspectingHost is a host that is having its hardware scanned. InspectingHost string = "demo-inspecting" + // PreparingErrorHost is a host that started preparing but failed. + PreparingErrorHost string = "demo-preparing-error" + + // PreparingHost is a host that is in the middle of preparing. + PreparingHost string = "demo-preparing" + // ValidationErrorHost is a host that started provisioning but // failed validation. ValidationErrorHost string = "demo-validation-error" @@ -184,6 +190,29 @@ func (p *demoProvisioner) UpdateHardwareState() (hwState provisioner.HardwareSta return } +// Prepare remove existing configuration and set new configuration +func (p *demoProvisioner) Prepare(unprepared bool) (result provisioner.Result, started bool, err error) { + hostName := p.host.ObjectMeta.Name + p.log.Info("provisioning image to host", "state", p.host.Status.Provisioning.State) + + switch hostName { + + case PreparingErrorHost: + p.log.Info("preparing error host") + result.ErrorMessage = "preparing failed" + + case PreparingHost: + p.log.Info("preparing host") + result.Dirty = true + result.RequeueAfter = time.Second * 5 + + default: + p.log.Info("finished preparing") + } + + return result, false, nil +} + // Adopt allows an externally-provisioned server to be adopted. func (p *demoProvisioner) Adopt(force bool) (result provisioner.Result, err error) { p.log.Info("adopting host") diff --git a/pkg/provisioner/empty/empty.go b/pkg/provisioner/empty/empty.go index e3b0c87551..ee10806787 100644 --- a/pkg/provisioner/empty/empty.go +++ b/pkg/provisioner/empty/empty.go @@ -46,6 +46,11 @@ func (p *emptyProvisioner) Adopt(force bool) (provisioner.Result, error) { return provisioner.Result{}, nil } +// Prepare remove existing configuration and set new configuration +func (p *emptyProvisioner) Prepare(unprepared bool) (result provisioner.Result, started bool, err error) { + return provisioner.Result{}, false, nil +} + // Provision writes the image from the host spec to the host. It may // be called multiple times, and should return true for its dirty flag // until the deprovisioning operation is completed. diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go index 3fb8f7a281..18609465ae 100644 --- a/pkg/provisioner/fixture/fixture.go +++ b/pkg/provisioner/fixture/fixture.go @@ -177,6 +177,12 @@ func (p *fixtureProvisioner) UpdateHardwareState() (hwState provisioner.Hardware return } +// Prepare remove existing configuration and set new configuration +func (p *fixtureProvisioner) Prepare(unprepared bool) (result provisioner.Result, started bool, err error) { + p.log.Info("preparing host") + return +} + // Adopt allows an externally-provisioned server to be adopted. func (p *fixtureProvisioner) Adopt(force bool) (result provisioner.Result, err error) { p.log.Info("adopting host") diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index 5fc067de75..c5eb765e4b 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -1199,6 +1199,93 @@ func (p *ironicProvisioner) ironicHasSameImage(ironicNode *nodes.Node) (sameImag return sameImage } +func (p *ironicProvisioner) buildManualCleaningSteps() (cleanSteps []nodes.CleanStep) { + // TODO: Add manual cleaning steps for host configuration + + return +} + +func (p *ironicProvisioner) startManualCleaning(ironicNode *nodes.Node) (success bool, result provisioner.Result, err error) { + cleanSteps := p.buildManualCleaningSteps() + + // Start manual clean + if len(cleanSteps) != 0 { + p.log.Info("remove existing configuration and set new configuration", "steps", cleanSteps) + return p.tryChangeNodeProvisionState( + ironicNode, + nodes.ProvisionStateOpts{ + Target: nodes.TargetClean, + CleanSteps: cleanSteps, + }, + ) + } + result, err = operationComplete() + return +} + +// Prepare remove existing configuration and set new configuration. +// If `started` is true, it means that we successfully executed `tryChangeNodeProvisionState`. +func (p *ironicProvisioner) Prepare(unprepared bool) (result provisioner.Result, started bool, err error) { + var ironicNode *nodes.Node + + if ironicNode, err = p.findExistingHost(); err != nil { + result, err = transientError(errors.Wrap(err, "could not find host to clean")) + return + } + if ironicNode == nil { + result, err = transientError(provisioner.NeedsRegistration) + return + } + + switch nodes.ProvisionState(ironicNode.ProvisionState) { + case nodes.Available: + if unprepared && len(p.buildManualCleaningSteps()) != 0 { + result, err = p.changeNodeProvisionState( + ironicNode, + nodes.ProvisionStateOpts{Target: nodes.TargetManage}, + ) + return + } + result, err = operationComplete() + + case nodes.Manageable: + if unprepared { + started, result, err = p.startManualCleaning(ironicNode) + return + } + // Manual clean finished + result, err = operationComplete() + + case nodes.CleanFail: + // When clean failed, we need to clean host provisioning settings. + // If unprepared is false, means the settings aren't cleared. + // So we can't set the node's state to manageable, until the settings are cleared. + if !unprepared { + result, err = operationFailed(ironicNode.LastError) + return + } + if ironicNode.Maintenance { + p.log.Info("clearing maintenance flag") + result, err = p.setMaintenanceFlag(ironicNode, false) + return + } + result, err = p.changeNodeProvisionState( + ironicNode, + nodes.ProvisionStateOpts{Target: nodes.TargetManage}, + ) + + case nodes.Cleaning, nodes.CleanWait: + p.log.Info("waiting for host to become manageable", + "state", ironicNode.ProvisionState, + "deploy step", ironicNode.DeployStep) + result, err = operationContinuing(provisionRequeueDelay) + + default: + result, err = transientError(fmt.Errorf("Have unexpected ironic node state %s", ironicNode.ProvisionState)) + } + return +} + // Provision writes the image from the host spec to the host. It may // be called multiple times, and should return true for its dirty flag // until the deprovisioning operation is completed. diff --git a/pkg/provisioner/ironic/prepare_test.go b/pkg/provisioner/ironic/prepare_test.go new file mode 100644 index 0000000000..79532c5ac0 --- /dev/null +++ b/pkg/provisioner/ironic/prepare_test.go @@ -0,0 +1,140 @@ +package ironic + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/stretchr/testify/assert" + + "github.com/metal3-io/baremetal-operator/pkg/bmc" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver" +) + +func TestPrepare(t *testing.T) { + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + unprepared bool + expectedStarted bool + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "manageable state(haven't clean steps)", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.Manageable), + UUID: nodeUUID, + }), + unprepared: true, + expectedStarted: false, + expectedRequestAfter: 0, + expectedDirty: false, + }, + // TODO: ADD test case when clean steps aren't empty + // { + // name: "manageable state(have clean steps)", + // ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + // ProvisionState: string(nodes.Manageable), + // UUID: nodeUUID, + // }), + // unprepared: true, + // expectedStarted: true, + // expectedRequestAfter: 10, + // expectedDirty: true, + // }, + { + name: "cleanFail state(cleaned provision settings)", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.CleanFail), + UUID: nodeUUID, + }), + expectedStarted: false, + expectedRequestAfter: 0, + expectedDirty: false, + }, + { + name: "cleanFail state(set ironic host to manageable)", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.CleanFail), + UUID: nodeUUID, + }), + unprepared: true, + expectedStarted: false, + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "cleaning state", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.Cleaning), + UUID: nodeUUID, + }), + expectedStarted: false, + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "cleanWait state", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.CleanWait), + UUID: nodeUUID, + }), + expectedStarted: false, + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "manageable state(manual clean finished)", + ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{ + ProvisionState: string(nodes.Manageable), + UUID: nodeUUID, + }), + expectedStarted: false, + expectedRequestAfter: 0, + expectedDirty: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, started, err := prov.Prepare(tc.unprepared) + + assert.Equal(t, tc.expectedStarted, started) + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 71a3da032d..85d773608d 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -61,6 +61,9 @@ type Provisioner interface { // the provisioner. Adopt(force bool) (result Result, err error) + // Prepare remove existing configuration and set new configuration + Prepare(unprepared bool) (result Result, started bool, err error) + // Provision writes the image from the host spec to the host. It // may be called multiple times, and should return true for its // dirty flag until the deprovisioning operation is completed.