Skip to content

Commit

Permalink
server and client
Browse files Browse the repository at this point in the history
Signed-off-by: Artem Bortnikov <[email protected]>
  • Loading branch information
aobort committed Nov 7, 2024
1 parent 66f5a96 commit f96d89f
Show file tree
Hide file tree
Showing 12 changed files with 536 additions and 145 deletions.
2 changes: 1 addition & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 46 additions & 5 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os"
"time"

"github.com/ironcore-dev/metal-operator/internal/executor"
"github.com/ironcore-dev/metal-operator/fmi"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
Expand Down Expand Up @@ -66,9 +66,27 @@ func main() {
powerPollingInterval time.Duration
powerPollingTimeout time.Duration
discoveryTimeout time.Duration
fmiServerAddress string
fmiServerProtocol string
fmiClientCAFile string
fmiClientCertFile string
fmiClientKeyFile string
fmiClientInsecure bool
fmiServerShutdownTimeout time.Duration
serverBIOSResyncInterval time.Duration
)

flag.StringVar(&fmiServerAddress, "fmi-server-address", "localhost:11000",
"The address the BIOS task runner binds to.")
flag.StringVar(&fmiServerProtocol, "fmi-server-protocol", "http", "The protocol of the BIOS task runner.")
flag.StringVar(&fmiClientCAFile, "fmi-client-ca-file", "", "File containing the CA to verify the BIOS task runner.")
flag.StringVar(&fmiClientCertFile, "fmi-client-cert-file", "",
"File containing the client certificate for the BIOS task runner.")
flag.StringVar(&fmiClientKeyFile, "fmi-client-key-file", "",
"File containing the client key for the BIOS task runner.")
flag.BoolVar(&fmiClientInsecure, "fmi-client-insecure", true, "Skip TLS verification for the BIOS task runner.")
flag.DurationVar(&fmiServerShutdownTimeout, "fmi-server-shutdown-timeout", 5*time.Second,
"Timeout for FMI server graceful shutdown")
flag.DurationVar(&serverBIOSResyncInterval, "server-bios-resync-interval", 10*time.Second, "Timeout for BIOS resync")
flag.DurationVar(&discoveryTimeout, "discovery-timeout", 30*time.Minute, "Timeout for discovery boot")
flag.DurationVar(&powerPollingInterval, "power-polling-interval", 5*time.Second,
Expand Down Expand Up @@ -187,6 +205,21 @@ func main() {
os.Exit(1)
}

biosTaskRunnerClient, err := fmi.NewClientForConfig(fmi.ClientConfig{
ServerURL: fmt.Sprintf("%s://%s", fmiServerProtocol, fmiServerAddress),
ScanEndpoint: "scan",
SettingsApplyEndpoint: "settings-apply",
VersionUpdateEndpoint: "version-update",
CAFile: fmiClientCAFile,
CertFile: fmiClientCertFile,
KeyFile: fmiClientKeyFile,
InsecureSkipVerify: fmiClientInsecure,
})
if err != nil {
setupLog.Error(err, "unable to create FMI client")
os.Exit(1)
}

if err = (&controller.EndpointReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down Expand Up @@ -245,10 +278,9 @@ func main() {
os.Exit(1)
}
if err = (&controller.ServerBIOSReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
// TODO define the executor to use by command-line flag
TaskExecutor: executor.New(mgr.GetClient(), insecure),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
TaskExecutor: biosTaskRunnerClient,
RequeueInterval: serverBIOSResyncInterval,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServerBIOS")
Expand Down Expand Up @@ -288,6 +320,15 @@ func main() {
}
}()

setupLog.Info("starting FMI server", "FMIServerAddress", fmiServerAddress)
biosTaskRunner := fmi.NewDefaultFMIServer(fmiServerAddress, mgr.GetClient(), fmiServerShutdownTimeout, insecure)
go func() {
if err := biosTaskRunner.Start(ctx); err != nil {
setupLog.Error(err, "problem running FMI server")
os.Exit(1)
}
}()

setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
Expand Down
81 changes: 81 additions & 0 deletions fmi/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package fmi

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)

type Client struct {
*http.Client
serverURL string
scanEndpoint string
settingsApplyEndpoint string
versionUpdateEndpoint string
}

func (c *Client) Scan(_ context.Context, serverBIOSRef string) (ScanResult, error) {
jsonBody, err := json.Marshal(TaskPayload{ServerBIOSRef: serverBIOSRef})
if err != nil {
return ScanResult{}, err
}

resp, err := c.Post(fmt.Sprintf("%s/%s", c.serverURL, c.scanEndpoint), "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return ScanResult{}, err
}
if resp.StatusCode != http.StatusOK {
return ScanResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
defer func() {
_ = resp.Body.Close()
}()

var result ScanResult
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return ScanResult{}, err
}
return result, nil
}

func (c *Client) SettingsApply(_ context.Context, serverBIOSRef string) error {
jsonBody, err := json.Marshal(TaskPayload{ServerBIOSRef: serverBIOSRef})
if err != nil {
return err
}

resp, err := c.Post(
fmt.Sprintf("%s/%s", c.serverURL, c.settingsApplyEndpoint), "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
defer func() {
_ = resp.Body.Close()
}()
return nil
}

func (c *Client) VersionUpdate(_ context.Context, serverBIOSRef string) error {
jsonBody, err := json.Marshal(TaskPayload{ServerBIOSRef: serverBIOSRef})
if err != nil {
return err
}

resp, err := c.Post(
fmt.Sprintf("%s/%s", c.serverURL, c.versionUpdateEndpoint), "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
defer func() {
_ = resp.Body.Close()
}()
return nil
}
152 changes: 152 additions & 0 deletions fmi/default_runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package fmi

import (
"context"
"fmt"
"sync"

metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
"github.com/ironcore-dev/metal-operator/internal/bmcutils"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// DefaultTaskRunner is the default implementation of the TaskRunner interface.
type DefaultTaskRunner struct {
client.Client
insecure bool
tasks *sync.Map
}

// NewDefaultTaskRunner creates a new DefaultTaskRunner.
func NewDefaultTaskRunner(client client.Client, insecure bool) *DefaultTaskRunner {
return &DefaultTaskRunner{
Client: client,
insecure: insecure,
tasks: &sync.Map{},
}
}

// ExecuteScan executes a scan task.
func (s *DefaultTaskRunner) ExecuteScan(ctx context.Context, serverBIOSRef string) (ScanResult, error) {
serverBIOS, server, err := s.getObjects(ctx, serverBIOSRef)
if err != nil {
return ScanResult{}, err
}
bmcClient, err := bmcutils.GetBMCClientForServer(ctx, s.Client, server, s.insecure)
if err != nil {
return ScanResult{}, err
}
currentBIOSVersion, err := bmcClient.GetBiosVersion(server.Spec.UUID)
if err != nil {
return ScanResult{}, err
}
attributes := make([]string, 0)
for k := range serverBIOS.Spec.BIOS.Settings {
attributes = append(attributes, k)
}
currentSettings, err := bmcClient.GetBiosAttributeValues(server.Spec.UUID, attributes)
if err != nil {
return ScanResult{}, err
}
return ScanResult{
Version: currentBIOSVersion,
Settings: currentSettings,
}, nil
}

// ExecuteSettingsApply applies the settings to the server.
func (s *DefaultTaskRunner) ExecuteSettingsApply(ctx context.Context, serverBIOSRef string) error {
inProgress, err := s.isTaskInProgress(ctx, serverBIOSRef)
if err != nil {
return err
}
if inProgress {
return nil
}

serverBIOS, server, err := s.getObjects(ctx, serverBIOSRef)
if err != nil {
return err
}
bmcClient, err := bmcutils.GetBMCClientForServer(ctx, s.Client, server, s.insecure)
if err != nil {
return err
}
defer bmcClient.Logout()

diff := make(map[string]string)
for k, v := range serverBIOS.Spec.BIOS.Settings {
if vv, ok := serverBIOS.Status.BIOS.Settings[k]; ok && vv == v {
continue
}
diff[k] = v
}
reset, err := bmcClient.SetBiosAttributes(server.Spec.UUID, diff)
if err != nil {
return err
}
if reset {
if err = s.patchServerCondition(ctx, server); err != nil {
return fmt.Errorf("failed to patch Server status: %w", err)
}
}
return nil
}

// todo: remove nolint

// ExecuteVersionUpdate updates the BIOS version of the server.
// nolint:unparam
func (s *DefaultTaskRunner) ExecuteVersionUpdate(_ context.Context, _ string) error {
return fmt.Errorf("not implemented")
}

// isTaskInProgress checks if a task is in progress.
// nolint:unparam
func (s *DefaultTaskRunner) isTaskInProgress(_ context.Context, serverBIOSRef string) (bool, error) {
_, ok := s.tasks.Load(serverBIOSRef)
return ok, nil
}

// storeTask stores a task for the given serverBIOSRef in the tasks map.
// nolint:unused
func (s *DefaultTaskRunner) storeTask(serverBIOSRef string) {
s.tasks.Store(serverBIOSRef, struct{}{})
}

// dropTask drops a task for the given serverBIOSRef from the tasks map.
// nolint:unused
func (s *DefaultTaskRunner) dropTask(serverBIOSRef string) {
s.tasks.Delete(serverBIOSRef)
}

// getObjects returns the ServerBIOS and Server objects for the given serverBIOSRef.
func (s *DefaultTaskRunner) getObjects(
ctx context.Context,
serverBIOSRef string,
) (*metalv1alpha1.ServerBIOS, *metalv1alpha1.Server, error) {
serverBIOS := &metalv1alpha1.ServerBIOS{}
if err := s.Get(ctx, client.ObjectKey{Name: serverBIOSRef}, serverBIOS); err != nil {
return nil, nil, err
}
server := &metalv1alpha1.Server{}
if err := s.Get(ctx, client.ObjectKey{Name: serverBIOS.Spec.ServerRef.Name}, server); err != nil {
return nil, nil, err
}
return serverBIOS, server, nil
}

// patchServerCondition patches the Server status with the given condition.
func (s *DefaultTaskRunner) patchServerCondition(ctx context.Context, server *metalv1alpha1.Server) error {
serverBase := server.DeepCopy()
changed := meta.SetStatusCondition(&server.Status.Conditions, metav1.Condition{
Type: "RebootRequired",
})
if changed {
return s.Status().Patch(ctx, serverBase, client.MergeFrom(server))

}
return nil
}
Loading

0 comments on commit f96d89f

Please sign in to comment.