stmcginnis / gofish

Gofish is a Golang client library for DMTF Redfish and SNIA Swordfish interaction.
BSD 3-Clause "New" or "Revised" License
211 stars 113 forks source link

Not able to see firmware update APIs #134

Closed sanverm2 closed 2 years ago

sanverm2 commented 3 years ago

Hi Sean,

We are trying to use gofish for management and monitoring our servers. However, we are able to use monitoring APIs but we do not see any implementation of firmware update as of now in version 0.9.0.

We checked checked APIs:

https://pkg.go.dev/github.com/stmcginnis/gofish/redfish#UpdateService

Is it under implementation?

stmcginnis commented 3 years ago

Hey @sanverm2, I believe others are using the UpdateService you pointed to successfully. Is there an issue you are running in to with it?

stmcginnis commented 3 years ago

Access would be something like:

package main

import (
    "github.com/stmcginnis/gofish"
)

func main() {
    // Create a new instance of gofish client, ignoring self-signed certs
    config := gofish.ClientConfig{
        Endpoint: "https://bmc-ip",
        Username: "my-username",
        Password: "my-password",
        Insecure: true,
    }
    c, err := gofish.Connect(config)
    if err != nil {
        panic(err)
    }
    defer c.Logout()

    // Retrieve the service root
    service := c.Service

    updateService, err := service.UpdateService()
    if err != nil {
        panic(err)
    }
}
mikeletux commented 3 years ago

Hi @sanverm2, I submitted PR #132 which solves an issue with the multipart method that for some reason was preventing from uploading FW packages correctly to the FW inventory.

Now, to use SImpleUpdate action you need to use custom headers to get Etag from the FW inventory and later on send the PostMultipart with custom headers using the if-match for that purpose.

I submitted another PR that implements all CRUD methods with WithCustomHeaders that allows to do that. Unfortunately the library version that comes with those features is not yet published, so you cannot get it from Go. Still, it is on the master branch for you to check.

stmcginnis commented 3 years ago

Thanks @mikeletux!

It would probably be a good idea to implement a helper method (something like UpdateService.SubmitUpdate(data []bytes)) to hide some of the plumbing from the consumer. If anyone has the time to take a crack at that, I think it would be a great addition to gofish.

mikeletux commented 3 years ago

Hey @stmcginnis ,

Yeah I agree, it would be awesome to implement a helper method like that to hide using Post call directly. I take note of that and try to make it real as soon as I can๐Ÿ˜„

stmcginnis commented 3 years ago

Awesome - thanks again @mikeletux!

sanverm2 commented 3 years ago

Thanks @stmcginnis @mikeletux .

"Now, to use SImpleUpdate action you need to use custom headers to get Etag from the FW inventory and later on send the PostMultipart with custom headers using the if-match for that purpose."

If you have sample code to use it, it would be great help.

mikeletux commented 3 years ago

@sanverm2 I'm writing a terraform provider for redfish. The code I'm writing for SimpleUpdate can be found @ https://github.com/mikeletux/terraform-provider-redfish/blob/master/redfish/resource_simple_update.go

Hope it helps (Some of the functions are not yet available on gofish v0.9.0 but it does on master branch)

func createRedfishSimpleUpdate(service *gofish.Service, d *schema.ResourceData) diag.Diagnostics {
    var diags diag.Diagnostics

    transferProtocol := d.Get("transfer_protocol").(string)
    targetFirmwareImage := d.Get("target_firmware_image").(string)

    // Get update service from root
    updateService, err := service.UpdateService()
    if err != nil {
        return diag.Errorf("error while retrieving UpdateService - %s", err)
    }

    //Check if the transfer protocol is available in the redfish instance
    err = checkTransferProtocol(transferProtocol, updateService)
    if err != nil {
        return diag.Errorf("%s", err)
    }

    switch transferProtocol {
    case "HTTP":
        // Get ETag from FW inventory
        response, err := service.Client.Get(updateService.FirmwareInventory)
        if err != nil {
            diag.Errorf("error while retrieving Etag from FirmwareInventory")
        }
        response.Body.Close()
        etag := response.Header.Get("ETag")

        // Set custom headers
        customHeaders := map[string]string{
            "if-match": etag,
        }

        // Open file to upload
        file, err := openFile(targetFirmwareImage)
        if err != nil {
            return diag.Errorf("couldn't open FW file to upload - %s", err)
        }
        defer file.Close()

        // Set payload
        payload := map[string]io.Reader{
            "file": file,
        }

        // Upload FW Package to FW inventory
        response, err = service.Client.PostMultipartWithHeaders(updateService.HTTPPushURI, payload, customHeaders)
        if err != nil {
            return diag.Errorf("there was an issue when uploading FW package to redfish - %s", err)
        }
        response.Body.Close()
        packageLocation := response.Header.Get("Location")

        // Get package information ( SoftwareID - Version )
        packageInformation, err := redfish.GetSoftwareInventory(service.Client, packageLocation)
        if err != nil {
            return diag.Errorf("there was an issue when retrieving uploaded package information - %s", err)
        }

        // Set payload for POST call that'll trigger the update job scheduling
        triggerUpdatePayload := struct {
            ImageURI string
        }{
            ImageURI: packageLocation,
        }
        // Do the POST call agains Simple.Update service
        response, err = service.Client.Post(updateService.UpdateServiceTarget, triggerUpdatePayload)
        if err != nil {
            // Delete uploaded package - TBD
            return diag.Errorf("there was an issue when scheduling the update job - %s", err)
        }
        response.Body.Close()

        // AT THIS POINT FW UPDATE WILL BE SCHEDULED
        d.Set("software_id", packageInformation.SoftwareID)
        d.Set("version", packageInformation.Version)
        d.SetId("test")

    default:
        return diag.Errorf("Transfer protocol not available in this implementation")

    }

    return diags
}

And also some helper functions that I use:

// openFile is a simple function that opens a file
func openFile(filePath string) (*os.File, error) {
    if f, err := os.Open(filePath); err != nil {
        return nil, fmt.Errorf("error when opening %s file - %s", filePath, err)
    } else {
        return f, nil
    }
}

// checkTransferProtocol checks if the chosen transfer protocol is available in the redfish instance
func checkTransferProtocol(transferProtocol string, updateService *redfish.UpdateService) error {
    for _, v := range updateService.TransferProtocol {
        if transferProtocol == v {
            return nil
        }
    }
    return fmt.Errorf("This transfer protocol is not available in this redfish instance")
}

Hope it helps ๐Ÿ˜„

sanverm2 commented 3 years ago

Awesome! Thanks @mikeletux

stmcginnis commented 2 years ago

I believe this is addressed. If not, please reopen or file a new issue with current specific details. Thanks!