From 5473226a6fd6cc02b262e6dff8f30915ff676c94 Mon Sep 17 00:00:00 2001 From: Iury Gregory Melo Ferreira Date: Thu, 1 Feb 2024 10:14:34 -0300 Subject: [PATCH] HostFirmwareComponents CRD and Controller - New Controller for HostFirmwareComponents - Updated the CRD based on kustomization - added GetFirmwareComponentsForNode to the Provisioner interface - implemented GetFirmwareComponentsForNode in ironic.go - Tests for HFC Controller Signed-off-by: Iury Gregory Melo Ferreira --- PROJECT | 3 +- .../v1alpha1/hostfirmwarecomponents_types.go | 9 +- .../hostfirmwarecomponents_types_test.go | 1 - .../metal3.io_hostfirmwarecomponents.yaml | 5 - config/base/crds/kustomization.yaml | 3 + ...cainjection_in_hostfirmwarecomponents.yaml | 7 + .../webhook_in_hostfirmwarecomponents.yaml | 16 + .../hostfirmwarecomponents_editor_role.yaml | 17 +- .../hostfirmwarecomponents_viewer_role.yaml | 11 +- config/base/rbac/role.yaml | 26 + config/render/capm3.yaml | 207 +++++++ ...l3.io_v1alpha1_hostfirmwarecomponents.yaml | 8 +- .../metal3.io/baremetalhost_controller.go | 3 +- .../metal3.io/host_state_machine_test.go | 4 + .../hostfirmwarecomponents_controller.go | 328 +++++++++++ .../metal3.io/hostfirmwarecomponents_test.go | 550 ++++++++++++++++++ main.go | 9 + pkg/provisioner/demo/demo.go | 4 + pkg/provisioner/fixture/fixture.go | 4 + pkg/provisioner/ironic/ironic.go | 47 ++ pkg/provisioner/provisioner.go | 3 + 21 files changed, 1243 insertions(+), 22 deletions(-) create mode 100644 config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml create mode 100644 config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml create mode 100644 controllers/metal3.io/hostfirmwarecomponents_controller.go create mode 100644 controllers/metal3.io/hostfirmwarecomponents_test.go diff --git a/PROJECT b/PROJECT index 01f6bffc68..192a602113 100644 --- a/PROJECT +++ b/PROJECT @@ -64,10 +64,11 @@ resources: - api: crdVersion: v1 namespaced: true + controller: true domain: metal3.io group: metal3.io kind: HostFirmwareComponents - path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha + path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1 version: v1alpha1 - api: crdVersion: v1 diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go index c4d3ad5f9a..4cd1014342 100644 --- a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go +++ b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go @@ -22,8 +22,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FirmwareUpdate defines a firmware update specification. type FirmwareUpdate struct { Component string `json:"component"` @@ -34,9 +32,9 @@ type FirmwareUpdate struct { type FirmwareComponentStatus struct { Component string `json:"component"` InitialVersion string `json:"initialVersion"` - CurrentVersion string `json:"currentVersion"` - LastVersionFlashed string `json:"lastVersionFlashed"` - UpdatedAt metav1.Time `json:"updatedAt"` + CurrentVersion string `json:"currentVersion,omitempty"` + LastVersionFlashed string `json:"lastVersionFlashed,omitempty"` + UpdatedAt metav1.Time `json:"updatedAt,omitempty"` } type UpdatesConditionType string @@ -77,7 +75,6 @@ type HostFirmwareComponentsStatus struct { } //+kubebuilder:object:root=true -//+kubebuilder:resource:shortName=hfc //+kubebuilder:subresource:status // HostFirmwareComponents is the Schema for the hostfirmwarecomponents API. diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go index e3b3ab858c..649a49334f 100644 --- a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go +++ b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go @@ -24,7 +24,6 @@ func TestValidateHostFirmwareComponents(t *testing.T) { Component: "bios", InitialVersion: "1.0", CurrentVersion: "1.0", - UpdatedAt: metav1.NewTime(time.Now()), }, { Component: "bmc", diff --git a/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml index 292348e573..095763de1f 100644 --- a/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml +++ b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml @@ -11,8 +11,6 @@ spec: kind: HostFirmwareComponents listKind: HostFirmwareComponentsList plural: hostfirmwarecomponents - shortNames: - - hfc singular: hostfirmwarecomponents scope: Namespaced versions: @@ -77,10 +75,7 @@ spec: type: string required: - component - - currentVersion - initialVersion - - lastVersionFlashed - - updatedAt type: object type: array conditions: diff --git a/config/base/crds/kustomization.yaml b/config/base/crds/kustomization.yaml index 3aae069920..7912eb8152 100644 --- a/config/base/crds/kustomization.yaml +++ b/config/base/crds/kustomization.yaml @@ -3,6 +3,7 @@ # It should be run by config/default resources: - bases/metal3.io_baremetalhosts.yaml +- bases/metal3.io_hostfirmwarecomponents.yaml - bases/metal3.io_hostfirmwaresettings.yaml - bases/metal3.io_firmwareschemas.yaml - bases/metal3.io_preprovisioningimages.yaml @@ -15,6 +16,7 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_baremetalhosts.yaml +#- patches/webhook_in_hostfirmwarecomponents.yaml #- patches/webhook_in_hostfirmwaresettings.yaml #- patches/webhook_in_firmwareschemas.yaml #- patches/webhook_in_preprovisioningimages.yaml @@ -26,6 +28,7 @@ patches: # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD - path: patches/cainjection_in_baremetalhosts.yaml +#- patches/cainjection_in_hostfirmwarecomponents.yaml #- patches/cainjection_in_hostfirmwaresettings.yaml #- patches/cainjection_in_firmwareschemas.yaml #- patches/cainjection_in_preprovisioningimages.yaml diff --git a/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml b/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml new file mode 100644 index 0000000000..cb970cf5d8 --- /dev/null +++ b/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: hostfirmwarecomponents.metal3.io diff --git a/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml b/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml new file mode 100644 index 0000000000..e77b92c7c7 --- /dev/null +++ b/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: hostfirmwarecomponents.metal3.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/base/rbac/hostfirmwarecomponents_editor_role.yaml b/config/base/rbac/hostfirmwarecomponents_editor_role.yaml index ae97a230e2..1646a289e2 100644 --- a/config/base/rbac/hostfirmwarecomponents_editor_role.yaml +++ b/config/base/rbac/hostfirmwarecomponents_editor_role.yaml @@ -2,6 +2,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: hostfirmwarecomponents-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: baremetal-operator + app.kubernetes.io/part-of: baremetal-operator + app.kubernetes.io/managed-by: kustomize name: hostfirmwarecomponents-editor-role rules: - apiGroups: @@ -9,16 +16,16 @@ rules: resources: - hostfirmwarecomponents verbs: + - create + - delete - get - list - - watch - - create - - update - patch - - delete + - update + - watch - apiGroups: - metal3.io resources: - hostfirmwarecomponents/status verbs: - - get \ No newline at end of file + - get diff --git a/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml b/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml index 05cd38c6de..a511d3a3c2 100644 --- a/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml +++ b/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml @@ -1,7 +1,14 @@ -# permissions for end users to edit hostfirmwarecomponents. +# permissions for end users to view hostfirmwarecomponents. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: hostfirmwarecomponents-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: baremetal-operator + app.kubernetes.io/part-of: baremetal-operator + app.kubernetes.io/managed-by: kustomize name: hostfirmwarecomponents-viewer-role rules: - apiGroups: @@ -17,4 +24,4 @@ rules: resources: - hostfirmwarecomponents/status verbs: - - get \ No newline at end of file + - get diff --git a/config/base/rbac/role.yaml b/config/base/rbac/role.yaml index 837707e704..0094daea27 100644 --- a/config/base/rbac/role.yaml +++ b/config/base/rbac/role.yaml @@ -109,6 +109,32 @@ rules: - patch - update - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents/finalizers + verbs: + - update +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents/status + verbs: + - get + - patch + - update - apiGroups: - metal3.io resources: diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml index 1f5ea863fa..518a174da2 100644 --- a/config/render/capm3.yaml +++ b/config/render/capm3.yaml @@ -1601,6 +1601,187 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: hostfirmwarecomponents.metal3.io +spec: + group: metal3.io + names: + kind: HostFirmwareComponents + listKind: HostFirmwareComponentsList + plural: hostfirmwarecomponents + singular: hostfirmwarecomponents + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HostFirmwareComponents is the Schema for the hostfirmwarecomponents + API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: HostFirmwareComponentsSpec defines the desired state of HostFirmwareComponents. + properties: + updates: + items: + description: FirmwareUpdate defines a firmware update specification. + properties: + component: + type: string + url: + type: string + required: + - component + - url + type: object + type: array + required: + - updates + type: object + status: + description: HostFirmwareComponentsStatus defines the observed state of + HostFirmwareComponents. + properties: + components: + description: Components is the list of all available firmware components + and their information. + items: + description: FirmwareComponentStatus defines the status of a firmware + component. + properties: + component: + type: string + currentVersion: + type: string + initialVersion: + type: string + lastVersionFlashed: + type: string + updatedAt: + format: date-time + type: string + required: + - component + - initialVersion + type: object + type: array + conditions: + description: Track whether updates stored in the spec are valid based + on the schema + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUpdated: + description: Time that the status was last updated + format: date-time + type: string + updates: + description: Updates is the list of all firmware components that should + be updated they are specified via name and url fields. + items: + description: FirmwareUpdate defines a firmware update specification. + properties: + component: + type: string + url: + type: string + required: + - component + - url + type: object + type: array + required: + - components + - updates + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.12.1 @@ -2103,6 +2284,32 @@ rules: - patch - update - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents/finalizers + verbs: + - update +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents/status + verbs: + - get + - patch + - update - apiGroups: - metal3.io resources: diff --git a/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml b/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml index 3283a8c7a4..f5b2537d6c 100644 --- a/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml +++ b/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml @@ -1,7 +1,13 @@ apiVersion: metal3.io/v1alpha1 kind: HostFirmwareComponents metadata: - namespace: host3firmwarecomponents.metal3.io + labels: + app.kubernetes.io/name: hostfirmwarecomponents + app.kubernetes.io/instance: hostfirmwarecomponents-sample + app.kubernetes.io/part-of: baremetal-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: baremetal-operator + name: hostfirmwarecomponents-sample spec: updates: - name: bios diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go index 7b9d83460b..8dd6d9e4cc 100644 --- a/controllers/metal3.io/baremetalhost_controller.go +++ b/controllers/metal3.io/baremetalhost_controller.go @@ -93,10 +93,11 @@ func (info *reconcileInfo) publishEvent(reason, message string) { // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;delete // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch -// Allow for managing hostfirmwaresettings and firmwareschema +// Allow for managing hostfirmwaresettings, firmwareschema, bmceventsubscriptions and hostfirmwarecomponents //+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwaresettings,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups=metal3.io,resources=firmwareschemas,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups=metal3.io,resources=bmceventsubscriptions,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents,verbs=get;list;watch;create;update;patch // Reconcile handles changes to BareMetalHost resources. func (r *BareMetalHostReconciler) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) { diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go index fd02ecd2c0..987a134d0f 100644 --- a/controllers/metal3.io/host_state_machine_test.go +++ b/controllers/metal3.io/host_state_machine_test.go @@ -1397,6 +1397,10 @@ func (m *mockProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEvent return result, nil } +func (p *mockProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} + func TestUpdateBootModeStatus(t *testing.T) { testCases := []struct { Scenario string diff --git a/controllers/metal3.io/hostfirmwarecomponents_controller.go b/controllers/metal3.io/hostfirmwarecomponents_controller.go new file mode 100644 index 0000000000..3e6eedf03f --- /dev/null +++ b/controllers/metal3.io/hostfirmwarecomponents_controller.go @@ -0,0 +1,328 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/go-logr/logr" + metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + "github.com/metal3-io/baremetal-operator/pkg/provisioner" +) + +// HostFirmwareComponentsReconciler reconciles a HostFirmwareComponents object. +type HostFirmwareComponentsReconciler struct { + client.Client + Log logr.Logger + ProvisionerFactory provisioner.Factory +} + +// rhfcInfo is used to simplify the pass or arguments. +type rhfcInfo struct { + ctx context.Context + log logr.Logger + hfc *metal3api.HostFirmwareComponents + bmh *metal3api.BareMetalHost + events []corev1.Event +} + +type conditionReasonHFC string + +const ( + reasonInvalidComponent conditionReasonHFC = "InvalidComponent" + reasonValidComponent conditionReasonHFC = "OK" +) + +func (info *rhfcInfo) publishEvent(reason, message string) { + t := metav1.Now() + hfcEvent := corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: reason + "-", + Namespace: info.hfc.ObjectMeta.Namespace, + }, + InvolvedObject: corev1.ObjectReference{ + Kind: info.hfc.Kind, + Namespace: info.hfc.Namespace, + Name: info.hfc.Name, + UID: info.hfc.UID, + APIVersion: metal3api.GroupVersion.String(), + }, + Reason: reason, + Message: message, + Source: corev1.EventSource{ + Component: "metal3-hostfirmwarecomponents-controller", + }, + FirstTimestamp: t, + LastTimestamp: t, + Count: 1, + Type: corev1.EventTypeNormal, + ReportingController: "metal3.io/hostfirmwarecomponents-controller", + } + + info.events = append(info.events, hfcEvent) +} + +//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents/finalizers,verbs=update + +// Reconcile handles changes to HostFirmwareComponents resources. +func (r *HostFirmwareComponentsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + reqLogger := r.Log.WithValues("hostfirmwarecomponents", req.NamespacedName) + reqLogger.Info("start") + + // Get the corresponding baremetalhost in this namespace, if one doesn't exist don't continue processing + bmh := &metal3api.BareMetalHost{} + err = r.Get(ctx, req.NamespacedName, bmh) + if err != nil { + if k8serrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + reqLogger.Error(err, "could not get baremetal host, not running hostfirmwarecomponents reconciler") + return ctrl.Result{Requeue: true, RequeueAfter: resourceNotAvailableRetryDelay}, err + } + + if hasDetachedAnnotation(bmh) { + reqLogger.Info("the host is detached, not running hostfirmwarecomponents reconciler") + return ctrl.Result{Requeue: true, RequeueAfter: unmanagedRetryDelay}, nil + } + // If the reconciliation is paused, requeue + annotations := bmh.GetAnnotations() + + if _, ok := annotations[metal3api.PausedAnnotation]; ok { + reqLogger.Info("host is paused, no work to do") + return ctrl.Result{Requeue: true, RequeueAfter: subResourceNotReadyRetryDelay}, nil + } + + // Fetch the HostFirmwareComponents + hfc := &metal3api.HostFirmwareComponents{} + info := &rhfcInfo{ctx: ctx, log: reqLogger, hfc: hfc, bmh: bmh} + if err = r.Get(ctx, req.NamespacedName, hfc); err != nil { + // The HFC resource may have been deleted + if k8serrors.IsNotFound(err) { + reqLogger.Info("HostFirmwareComponents not found") + return ctrl.Result{Requeue: false}, err + } + // Error reading the object - requeue the request. + return ctrl.Result{}, fmt.Errorf("could not load hostFirmwareComponents: %w", err) + } + + // Create a provisioner to access Ironic API + prov, err := r.ProvisionerFactory.NewProvisioner(ctx, provisioner.BuildHostDataNoBMC(*bmh), info.publishEvent) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create provisioner: %w", err) + } + + ready, err := prov.TryInit() + if err != nil || !ready { + var msg string + if err == nil { + msg = "not ready" + } else { + msg = err.Error() + } + reqLogger.Info("provisioner is not ready", "Error", msg, "RequeueAfter", provisionerRetryDelay) + return ctrl.Result{Requeue: true, RequeueAfter: provisionerRetryDelay}, nil + } + + newStatus, err := r.updateHostFirmware(info) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not update hostfirmwarecomponents: %w", err) + } + + // Check ironic for the components information if possible + components, err := prov.GetFirmwareComponents() + info.log.Info("retrieving firmware components and saving to resource", "Node", bmh.Status.Provisioning.ID) + + if err != nil { + reqLogger.Error(err, "provisioner returns error", "RequeueAfter", provisionerRetryDelay) + setUpdatesCondition(info.hfc.GetGeneration(), &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reasonInvalidComponent, err.Error()) + return ctrl.Result{Requeue: true, RequeueAfter: provisionerRetryDelay}, err + } + + if err = r.updateHostFirmwareComponents(newStatus, components, info); err != nil { + return ctrl.Result{Requeue: false}, err + } + + for _, e := range info.events { + r.publishEvent(info.ctx, req, e) + } + + if meta.IsStatusConditionTrue(info.hfc.Status.Conditions, string(metal3api.HostFirmwareComponentsChangeDetected)) { + return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelayChangeDetected}, nil + } + return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelay}, nil +} + +// Update the HostFirmwareComponents resource using the components from provisioner. +func (r *HostFirmwareComponentsReconciler) updateHostFirmware(info *rhfcInfo) (newStatus metal3api.HostFirmwareComponentsStatus, err error) { + dirty := false + + // change the Updates in Status + newStatus.Updates = info.hfc.Spec.Updates + + // Check if the updates in the Spec are different than Status + updatesMismatch := !reflect.DeepEqual(info.hfc.Status.Updates, info.hfc.Spec.Updates) + + reason := reasonValidComponent + generation := info.hfc.GetGeneration() + + if updatesMismatch { + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionTrue, reason, "") { + dirty = true + } + + err := r.validateHostFirmwareComponents(info) + if err != nil { + info.publishEvent("ValidationFailed", fmt.Sprintf("Invalid Firmware Components: %s", err)) + reason = reasonInvalidComponent + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reason, fmt.Sprintf("Invalid Firmware Components: %s", err)) { + dirty = true + } + } else if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionTrue, reason, "") { + dirty = true + } + } else { + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionTrue, reason, "") { + dirty = true + } + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionFalse, reason, "") { + dirty = true + } + } + + // Update Status if has changed + if dirty { + info.log.Info("Status for HostFirmwareComponents changed") + info.hfc.Status = *newStatus.DeepCopy() + + t := metav1.Now() + info.hfc.Status.LastUpdated = &t + return newStatus, r.Status().Update(info.ctx, info.hfc) + } + return newStatus, nil +} + +// Update the HostFirmwareComponents resource using the components from provisioner. +func (r *HostFirmwareComponentsReconciler) updateHostFirmwareComponents(newStatus metal3api.HostFirmwareComponentsStatus, components []metal3api.FirmwareComponentStatus, info *rhfcInfo) (err error) { + dirty := false + // change the Components in Status + newStatus.Components = components + // Check if the components information we retrieved is different from the one in Status + componentsInfoMismatch := !reflect.DeepEqual(components, info.hfc.Status.Components) + reason := reasonValidComponent + generation := info.hfc.GetGeneration() + // Log the components we have + info.log.Info("firmware components for node", "components", components, "bmh", info.bmh.Name) + if componentsInfoMismatch { + setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionTrue, reason, "") + dirty = true + } + if dirty { + info.log.Info("Components Status for HostFirmwareComponents changed") + info.hfc.Status = *newStatus.DeepCopy() + + t := metav1.Now() + info.hfc.Status.LastUpdated = &t + return r.Status().Update(info.ctx, info.hfc) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *HostFirmwareComponentsReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconcile int) error { + return ctrl.NewControllerManagedBy(mgr). + For(&metal3api.HostFirmwareComponents{}). + WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconcile}). + WithEventFilter( + predicate.Funcs{ + UpdateFunc: r.updateEventHandler, + }). + Complete(r) +} + +func (r *HostFirmwareComponentsReconciler) updateEventHandler(e event.UpdateEvent) bool { + r.Log.Info("hostfirmwarecomponents in event handler") + + return e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration() +} + +func (r *HostFirmwareComponentsReconciler) validateHostFirmwareComponents(info *rhfcInfo) []error { + var errors []error + allowedNames := map[string]struct{}{"bmc": {}, "bios": {}} + for _, update := range info.hfc.Spec.Updates { + componentName := update.Component + if _, ok := allowedNames[componentName]; !ok { + errors = append(errors, fmt.Errorf("component %s is invalid, only 'bmc' or 'bios' are allowed as update names", componentName)) + } + if len(errors) == 0 { + componentInStatus := false + for _, componentStatus := range info.hfc.Status.Components { + if componentName == componentStatus.Component { + componentInStatus = true + break + } + } + if !componentInStatus { + errors = append(errors, fmt.Errorf("component %s is invalid because is not present in status", componentName)) + } + } + } + + return errors +} + +func (r *HostFirmwareComponentsReconciler) publishEvent(ctx context.Context, request ctrl.Request, event corev1.Event) { + reqLogger := r.Log.WithValues("hostfirmwarecomponents", request.NamespacedName) + reqLogger.Info("publishing event", "reason", event.Reason, "message", event.Message) + err := r.Create(ctx, &event) + if err != nil { + reqLogger.Info("failed to record event, ignoring", + "reason", event.Reason, "message", event.Message, "error", err) + } +} + +func setUpdatesCondition(generation int64, status *metal3api.HostFirmwareComponentsStatus, info *rhfcInfo, + cond metal3api.UpdatesConditionType, newStatus metav1.ConditionStatus, + reason conditionReasonHFC, message string) bool { + newCondition := metav1.Condition{ + Type: string(cond), + Status: newStatus, + ObservedGeneration: generation, + Reason: string(reason), + Message: message, + } + currCond := meta.FindStatusCondition(info.hfc.Status.Conditions, string(cond)) + meta.SetStatusCondition(&status.Conditions, newCondition) + + if currCond == nil || currCond.Status != newStatus { + return true + } + return false +} diff --git a/controllers/metal3.io/hostfirmwarecomponents_test.go b/controllers/metal3.io/hostfirmwarecomponents_test.go new file mode 100644 index 0000000000..1998d1a760 --- /dev/null +++ b/controllers/metal3.io/hostfirmwarecomponents_test.go @@ -0,0 +1,550 @@ +package controllers + +import ( + "context" + "testing" + + metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Test support for HostFirmwareComponents in the HostFirmwareComponentsReconciler. +func getTestHFCReconciler(host *metal3api.HostFirmwareComponents) *HostFirmwareComponentsReconciler { + c := fakeclient.NewClientBuilder().WithRuntimeObjects(host).WithStatusSubresource(host).Build() + + reconciler := &HostFirmwareComponentsReconciler{ + Client: c, + Log: ctrl.Log.WithName("test_reconciler").WithName("HostFirmwareComponents"), + } + + return reconciler +} + +func getMockHFCProvisioner(components []metal3api.FirmwareComponentStatus) *hfcMockProvisioner { + return &hfcMockProvisioner{ + Components: components, + Error: nil, + } +} + +type hfcMockProvisioner struct { + Components []metal3api.FirmwareComponentStatus + Error error +} + +func (m *hfcMockProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) { + return m.Components, m.Error +} + +// Mock components to return from provisioner. +func getCurrentComponents(updatedComponents string) []metal3api.FirmwareComponentStatus { + var components []metal3api.FirmwareComponentStatus + switch updatedComponents { + case "bmc": + components = []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.0.1", + }, + } + case "bios": + components = []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.0.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + } + default: + components = []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + } + } + + return components +} + +// Create the baremetalhost reconciler and use that to create bmh in same namespace. +func createBaremetalHostHFC() *metal3api.BareMetalHost { + bmh := &metal3api.BareMetalHost{} + bmh.ObjectMeta = metav1.ObjectMeta{Name: "hostName", Namespace: "hostNamespace"} + c := fakeclient.NewFakeClient(bmh) + + reconciler := &BareMetalHostReconciler{ + Client: c, + ProvisionerFactory: nil, + Log: ctrl.Log.WithName("bmh_reconciler").WithName("BareMetalHost"), + } + reconciler.Create(context.TODO(), bmh) + + return bmh +} + +// Create and HFC with input spec components. +func getHFC(spec metal3api.HostFirmwareComponentsSpec) *metal3api.HostFirmwareComponents { + hfc := &metal3api.HostFirmwareComponents{} + + hfc.Status = metal3api.HostFirmwareComponentsStatus{ + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.0.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.0.1", + }, + }, + } + + hfc.TypeMeta = metav1.TypeMeta{ + Kind: "HostFirmwareComponents", + APIVersion: "metal3.io/v1alpha1"} + hfc.ObjectMeta = metav1.ObjectMeta{ + Name: hostName, + Namespace: hostNamespace} + + hfc.Spec = spec + return hfc +} + +// Test the hostfirmwarecomponents reconciler functions. +func TestStoreHostFirmwareComponents(t *testing.T) { + testCases := []struct { + Scenario string + UpdatedComponents string + CurrentHFCResource *metal3api.HostFirmwareComponents + ExpectedComponents *metal3api.HostFirmwareComponents + }{ + { + Scenario: "update bmc", + UpdatedComponents: "bmc", + CurrentHFCResource: &metal3api.HostFirmwareComponents{ + TypeMeta: metav1.TypeMeta{ + Kind: "HostFirmwareComponents", + APIVersion: "metal3.io/v1alpha1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "hostName", + Namespace: "hostNamespace", + ResourceVersion: "1"}, + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurls/newbmcfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.0.1", + }, + }, + }, + }, + ExpectedComponents: &metal3api.HostFirmwareComponents{ + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.0.1", + }, + }, + Conditions: []metav1.Condition{ + {Type: "ChangeDetected", Status: "True", Reason: "OK"}, + {Type: "Valid", Status: "True", Reason: "OK"}, + }, + }, + }, + }, + { + Scenario: "update bios", + UpdatedComponents: "bios", + CurrentHFCResource: &metal3api.HostFirmwareComponents{ + TypeMeta: metav1.TypeMeta{ + Kind: "HostFirmwareComponents", + APIVersion: "metal3.io/v1alpha1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "hostName", + Namespace: "hostNamespace", + ResourceVersion: "1"}, + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bios", + URL: "https://myurls/newbiosfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.0.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + }, + }, + }, + ExpectedComponents: &metal3api.HostFirmwareComponents{ + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.0.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + }, + Conditions: []metav1.Condition{ + {Type: "ChangeDetected", Status: "True", Reason: "OK"}, + {Type: "Valid", Status: "True", Reason: "OK"}, + }, + }, + }, + }, + { + Scenario: "update all", + UpdatedComponents: "all", + CurrentHFCResource: &metal3api.HostFirmwareComponents{ + TypeMeta: metav1.TypeMeta{ + Kind: "HostFirmwareComponents", + APIVersion: "metal3.io/v1alpha1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "hostName", + Namespace: "hostNamespace", + ResourceVersion: "1"}, + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + { + Component: "bios", + URL: "https://myurls/newbiosfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + }, + }, + }, + ExpectedComponents: &metal3api.HostFirmwareComponents{ + Spec: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + }, + Status: metal3api.HostFirmwareComponentsStatus{ + Updates: []metal3api.FirmwareUpdate{ + { + Component: "bmc", + URL: "https://myurl/newbmcfirmware", + }, + { + Component: "bios", + URL: "https://myurl/newbiosfirmware", + }, + }, + Components: []metal3api.FirmwareComponentStatus{ + { + Component: "bmc", + InitialVersion: "1.0.0", + CurrentVersion: "1.1.0", + LastVersionFlashed: "1.1.0", + }, + { + Component: "bios", + InitialVersion: "1.0.1", + CurrentVersion: "1.1.10", + LastVersionFlashed: "1.1.10", + }, + }, + Conditions: []metav1.Condition{ + {Type: "ChangeDetected", Status: "True", Reason: "OK"}, + {Type: "Valid", Status: "True", Reason: "OK"}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Scenario, func(t *testing.T) { + ctx := context.TODO() + prov := getMockHFCProvisioner(getCurrentComponents(tc.UpdatedComponents)) + + tc.ExpectedComponents.TypeMeta = metav1.TypeMeta{ + Kind: "HostFirmwareComponents", + APIVersion: "metal3.io/v1alpha1"} + tc.ExpectedComponents.ObjectMeta = metav1.ObjectMeta{ + Name: "hostName", + Namespace: "hostNamespace", + ResourceVersion: "2"} + + hfc := tc.CurrentHFCResource + r := getTestHFCReconciler(hfc) + // Create a bmh resource needed by hfc reconciler + bmh := createBaremetalHostHFC() + + info := &rhfcInfo{ + ctx: ctx, + log: logf.Log.WithName("controllers").WithName("HostFirmwareComponents"), + hfc: tc.CurrentHFCResource, + bmh: bmh, + } + + currentStatus, err := r.updateHostFirmware(info) + assert.NoError(t, err) + + components, err := prov.GetFirmwareComponents() + assert.NoError(t, err) + err = r.updateHostFirmwareComponents(currentStatus, components, info) + assert.NoError(t, err) + + // Check that resources get created or updated + key := client.ObjectKey{ + Namespace: hfc.ObjectMeta.Namespace, Name: hfc.ObjectMeta.Name} + actual := &metal3api.HostFirmwareComponents{} + err = r.Client.Get(ctx, key, actual) + assert.Equal(t, nil, err) + + // Ensure ExpectedComponents matches actual + assert.Equal(t, tc.ExpectedComponents.Spec.Updates, actual.Spec.Updates) + assert.Equal(t, tc.ExpectedComponents.Status.Components, actual.Status.Components) + assert.Equal(t, tc.ExpectedComponents.Status.Updates, actual.Status.Updates) + currentTime := metav1.Now() + tc.ExpectedComponents.Status.LastUpdated = ¤tTime + actual.Status.LastUpdated = ¤tTime + for i := range tc.ExpectedComponents.Status.Conditions { + tc.ExpectedComponents.Status.Conditions[i].LastTransitionTime = currentTime + actual.Status.Conditions[i].LastTransitionTime = currentTime + } + assert.Equal(t, tc.ExpectedComponents.Status.LastUpdated, actual.Status.LastUpdated) + assert.Equal(t, tc.ExpectedComponents.Status.Conditions, actual.Status.Conditions) + }) + } +} + +// Test the function to validate the components in the Spec. +func TestValidadeHostFirmwareComponents(t *testing.T) { + testCases := []struct { + Scenario string + SpecUpdates metal3api.HostFirmwareComponentsSpec + ExpectedErrors []string + }{ + { + Scenario: "valid spec - all components", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "bmc", URL: "https://myurl/mybmcfw"}, + {Component: "bios", URL: "https://myurl/mybiosfw"}, + }, + }, + ExpectedErrors: []string{""}, + }, + { + Scenario: "valid spec - only bios", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "bios", URL: "https://myurl/mybiosfw"}, + }, + }, + ExpectedErrors: []string{""}, + }, + { + Scenario: "valid spec - only bmc", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "bmc", URL: "https://myurl/mybmcfw"}, + }, + }, + ExpectedErrors: []string{""}, + }, + { + Scenario: "invalid nic component", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "nic", URL: "https://myurl/mynicfw"}, + }, + }, + ExpectedErrors: []string{"component nic is invalid, only 'bmc' or 'bios' are allowed as update names"}, + }, + { + Scenario: "invalid nic component with other valid components", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "bmc", URL: "https://myurl/mybmcfw"}, + {Component: "bios", URL: "https://myurl/mybiosfw"}, + {Component: "nic", URL: "https://myurl/mynicfw"}, + }, + }, + ExpectedErrors: []string{"component nic is invalid, only 'bmc' or 'bios' are allowed as update names"}, + }, + { + Scenario: "component not in lowercase", + SpecUpdates: metal3api.HostFirmwareComponentsSpec{ + Updates: []metal3api.FirmwareUpdate{ + {Component: "BMC", URL: "https://myurl/mybmcfw"}, + {Component: "BIOS", URL: "https://myurl/mybiosfw"}, + }, + }, + ExpectedErrors: []string{ + "component BMC is invalid, only 'bmc' or 'bios' are allowed as update names", + "component BIOS is invalid, only 'bmc' or 'bios' are allowed as update names", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Scenario, func(t *testing.T) { + ctx := context.TODO() + hfc := getHFC(tc.SpecUpdates) + r := getTestHFCReconciler(hfc) + info := rhfcInfo{ + ctx: ctx, + log: logf.Log.WithName("controllers").WithName("HostFirmwareComponents"), + hfc: hfc, + } + errors := r.validateHostFirmwareComponents(&info) + if len(errors) == 0 { + assert.Equal(t, tc.ExpectedErrors[0], "") + } else { + for i := range errors { + assert.Equal(t, tc.ExpectedErrors[i], errors[i].Error()) + } + } + }) + } +} diff --git a/main.go b/main.go index 47ce07e478..b5aa8d0533 100644 --- a/main.go +++ b/main.go @@ -291,6 +291,15 @@ func main() { os.Exit(1) } + if err = (&metal3iocontroller.HostFirmwareComponentsReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("HostFirmwareComponents"), + ProvisionerFactory: provisionerFactory, + }).SetupWithManager(mgr, maxConcurrency); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "HostFirmwareComponents") + os.Exit(1) + } + setupChecks(mgr) if enableWebhook { diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go index f6741975d5..77e61b0286 100644 --- a/pkg/provisioner/demo/demo.go +++ b/pkg/provisioner/demo/demo.go @@ -302,3 +302,7 @@ func (p *demoProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEventSu func (p *demoProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) { return result, nil } + +func (p *demoProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go index 8005d07ab4..6b004a3111 100644 --- a/pkg/provisioner/fixture/fixture.go +++ b/pkg/provisioner/fixture/fixture.go @@ -349,3 +349,7 @@ func (p *fixtureProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEven func (p *fixtureProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) { return result, nil } + +func (p *fixtureProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index 5fe505e837..a9e6c793bf 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -1152,6 +1152,53 @@ func (p *ironicProvisioner) GetFirmwareSettings(includeSchema bool) (settings me return settings, schema, nil } +// GetFirmwareComponents gets all available firmware components for a node and return a list. +func (p *ironicProvisioner) GetFirmwareComponents() ([]metal3api.FirmwareComponentStatus, error) { + ironicNode, err := p.getNode() + if err != nil { + return nil, fmt.Errorf("could not get node to retrieve firmware components: %w", err) + } + + if !p.availableFeatures.HasFirmwareUpdates() { + return nil, fmt.Errorf("current ironic version does not support firmware updates") + } + // Get the components from Ironic via Gophercloud + componentList, componentListErr := nodes.ListFirmware(p.ctx, p.client, ironicNode.UUID).Extract() + + if componentListErr != nil { + bmcAccess, _ := p.bmcAccess() + if bmcAccess.FirmwareInterface() == "no-firmware" { + return nil, fmt.Errorf("node %s is using firmware interface %s: %w", ironicNode.UUID, bmcAccess.FirmwareInterface(), componentListErr) + } + + return nil, fmt.Errorf("could not get firmware components for node %s: %w", ironicNode.UUID, componentListErr) + } + + // Setting to 2 since we only support bmc and bios + componentsInfo := make([]metal3api.FirmwareComponentStatus, 0, 2) + + // Iterate over the list of components to extract their information and update the list. + for _, fwc := range componentList { + if fwc.Component != "bios" && fwc.Component != "bmc" { + p.log.Info("ignoring firmware component for node", "component", fwc.Component, "node", ironicNode.UUID) + continue + } + component := metal3api.FirmwareComponentStatus{ + Component: fwc.Component, + InitialVersion: fwc.InitialVersion, + CurrentVersion: fwc.CurrentVersion, + LastVersionFlashed: fwc.LastVersionFlashed, + UpdatedAt: metav1.Time{ + Time: *fwc.UpdatedAt, + }, + } + componentsInfo = append(componentsInfo, component) + p.log.Info("firmware component added for node", "component", fwc.Component, "node", ironicNode.UUID) + } + + return componentsInfo, componentListErr +} + // 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 diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 29a15d54c1..7a43209f80 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -200,6 +200,9 @@ type Provisioner interface { // RemoveBMCEventSubscriptionForNode delete the subscription RemoveBMCEventSubscriptionForNode(subscription metal3api.BMCEventSubscription) (result Result, err error) + + // GetFirmwareComponents gets all firmware components available from a note + GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) } // Result holds the response from a call in the Provsioner API.