From 4f784b50eb695b6ff7eac06da89f8cfe9fe9d33a Mon Sep 17 00:00:00 2001 From: Christian Ege Date: Sat, 13 Apr 2024 17:46:12 +0200 Subject: [PATCH] feat: add a function to wait for the device to become ready Signed-off-by: Christian Ege --- cmd/ovp8xx/cmd/waitforonline.go | 52 +++++++++++++++++++++++++++++++++ pkg/ovp8xx/client.go | 43 +++++++++++++++++++++++++++ pkg/ovp8xx/rpc.go | 47 +++++++++++++++++++++++++---- 3 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 cmd/ovp8xx/cmd/waitforonline.go diff --git a/cmd/ovp8xx/cmd/waitforonline.go b/cmd/ovp8xx/cmd/waitforonline.go new file mode 100644 index 0000000..4c39a70 --- /dev/null +++ b/cmd/ovp8xx/cmd/waitforonline.go @@ -0,0 +1,52 @@ +/* +Copyright © 2023 Christian Ege +*/ +package cmd + +import ( + "fmt" + "time" + + "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/spf13/cobra" +) + +func waitForOnlineCommand(cmd *cobra.Command, args []string) error { + var ok = false + var err error + helper, err := NewHelper(cmd) + if err != nil { + return err + } + + o3r := ovp8xx.NewClient( + ovp8xx.WithHost(helper.hostname()), + ) + + timeout, err := cmd.Flags().GetUint("timeout") + if err != nil { + return err + } + + if ok, err = o3r.IsAvailable(time.Duration(timeout) * time.Second); err != nil { + return err + } + if ok { + fmt.Printf("The device is available now\n") + } + return nil +} + +// waitForOnlineCmd represents the wait command +var waitForOnlineCmd = &cobra.Command{ + Use: "WaitForOnline", + Short: "Wait until the device is accessible", + Long: `This command is maybe useful after a reboot or power on. +It can be used to wait until the device can handle requests`, + RunE: waitForOnlineCommand, +} + +func init() { + rootCmd.AddCommand(waitForOnlineCmd) + waitForOnlineCmd.Flags().Uint("timeout", 120, "Timeout in Seconds to wait until the device is accessible") +} diff --git a/pkg/ovp8xx/client.go b/pkg/ovp8xx/client.go index d8497f4..f6e45f6 100644 --- a/pkg/ovp8xx/client.go +++ b/pkg/ovp8xx/client.go @@ -1,7 +1,9 @@ package ovp8xx import ( + "context" "fmt" + "time" ) type ( @@ -16,6 +18,13 @@ type ( } ) +// NewClient creates a new OVP8xx client with the provided options. +// The opts parameter is a variadic parameter that allows specifying +// multiple client options. +// Example usage: +// +// client := NewClient(WithTimeout(10 * time.Second), WithRetry(3)) +// // ... func NewClient(opts ...ClientOption) *Client { // Initialise with default values client := &Client{ @@ -30,15 +39,49 @@ func NewClient(opts ...ClientOption) *Client { return client } +// WithHost sets the host for the OVP8xx client. +// It returns a ClientOption function that can be used to configure the client. func WithHost(host string) ClientOption { return func(c *Client) { c.host = host } } +// GetDiagnosticClient returns a new instance of DiagnosisClient that can be used to perform diagnostic operations on the OVP8xx device. func (device *Client) GetDiagnosticClient() *DiagnosisClient { client := &DiagnosisClient{} client.host = device.host client.url = fmt.Sprintf("http://%s/api/rpc/v1/com.ifm.diagnostic/", client.host) return client } + +// IsAvailable checks if the client is available by making a XML-RPC get request to query the "/device" object. +// This is useful to wait until a device is ready for communication. +// It returns a boolean indicating the availability status and an error if any. +// The function uses a timeout duration to limit the execution time of the request. +func (d *Client) IsAvailable(timeout time.Duration) (bool, error) { + var err error + proc := make(chan struct{}, 1) + + go func() { + for { + if _, err := d.Get([]string{"/device"}); err != nil { + // In case of an error retry, regardless of an timeout + continue + } + // we are done, the get call was successful + proc <- struct{}{} + } + }() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + select { + case <-ctx.Done(): + return false, fmt.Errorf("timeout occurred while checking if the device is available: %w", ctx.Err()) + case <-proc: + return true, err + } +} diff --git a/pkg/ovp8xx/rpc.go b/pkg/ovp8xx/rpc.go index 8490a5f..6fcb592 100644 --- a/pkg/ovp8xx/rpc.go +++ b/pkg/ovp8xx/rpc.go @@ -1,9 +1,26 @@ package ovp8xx import ( + "errors" + "net" + "alexejk.io/go-xmlrpc" ) +// IsTimeoutError checks if the given error is a network timeout error. +// It returns true if the error is a network error and the timeout flag is set, otherwise it returns false. +func IsTimeoutError(err error) bool { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return true + } + return false +} + +// Get retrieves the configuration for the specified pointers from the OVP8xx device. +// The pointers parameter is a slice of strings that contains the pointers to retrieve the configuration for. +// The function returns the retrieved configuration as a Config struct and an error if any occurred. +// Example usage: config, err := device.Get([]string{"/device", "/ports"}) func (device *Client) Get(pointers []string) (Config, error) { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -26,6 +43,8 @@ func (device *Client) Get(pointers []string) (Config, error) { return *NewConfig(WitJSONString(result.JSON)), nil } +// Set sets the configuration of the OVP8xx device. +// It takes a Config object as input and returns an error if any. func (device *Client) Set(conf Config) error { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -37,13 +56,11 @@ func (device *Client) Set(conf Config) error { Data string }{Data: conf.String()} - if err := client.Call("set", arg, nil); err != nil { - return err - } - - return nil + return client.Call("set", arg, nil) } +// GetInit retrieves the initial configuration from the OVP8xx device. +// It returns a Config struct representing the device configuration and an error if any. func (device *Client) GetInit() (Config, error) { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -62,6 +79,10 @@ func (device *Client) GetInit() (Config, error) { return *NewConfig(WitJSONString(result.JSON)), nil } +// SaveInit saves the configuration of the OVP8xx device. +// If no pointers are provided, it saves the complete configuration. +// If pointers are provided, it saves only the specified configuration pointers. +// It returns an error if there was a problem saving the configuration. func (device *Client) SaveInit(pointers []string) error { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -76,10 +97,15 @@ func (device *Client) SaveInit(pointers []string) error { arg := &struct { Pointers []string - }{Pointers: pointers} + }{ + Pointers: pointers, + } return client.Call("saveInit", arg, nil) } +// FactoryReset performs a factory reset on the OVP8xx device. +// If keepNetworkSettings is set to true, the network settings will be preserved after the reset. +// Returns an error if the factory reset fails or if there is an issue with the XML-RPC client. func (device *Client) FactoryReset(keepNetworkSettings bool) error { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -95,6 +121,9 @@ func (device *Client) FactoryReset(keepNetworkSettings bool) error { return client.Call("factoryReset", arg, nil) } +// GetSchema retrieves the schema for the specified pointers from the OVP8xx device. +// It returns the schema in JSON format as a string. +// If an error occurs during the retrieval process, it returns an empty string and the error. func (device *Client) GetSchema(pointers []string) (string, error) { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -114,6 +143,8 @@ func (device *Client) GetSchema(pointers []string) (string, error) { return result.JSON, nil } +// Reboot sends a reboot command to the OVP8xx device. +// If an error occurs during the connection or the method call, it is returned. func (device *Client) Reboot() error { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -137,6 +168,8 @@ func (device *Client) RebootToSWUpdate() error { return client.Call("rebootToRecovery", nil, nil) } +// GetFiltered retrieves a filtered configuration from the DiagnosisClient. +// It takes a Config object as input and returns a Config object and an error. func (device *DiagnosisClient) GetFiltered(conf Config) (Config, error) { client, err := xmlrpc.NewClient(device.url) if err != nil { @@ -159,6 +192,8 @@ func (device *DiagnosisClient) GetFiltered(conf Config) (Config, error) { return *NewConfig(WitJSONString(result.JSON)), nil } +// GetFilterSchema retrieves the filter schema from the DiagnosisClient. +// It returns a Config object representing the filter schema and an error if any. func (device *DiagnosisClient) GetFilterSchema() (Config, error) { client, err := xmlrpc.NewClient(device.url) if err != nil {