diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index 97ca951..4183b47 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -1,4 +1,25 @@ #!/bin/bash +# install missing packages +sudo apt-get update && + sudo apt-get install -q -y git-lfs \ + python3-pip \ + python3-venv \ + shfmt \ + shellcheck && + sudo apt-get clean && + sudo rm -rf /var/lib/apt/lists/* + +# Fetch git large files +git lfs fetch --all + +# Install Python packages +export PATH=~/.local/bin:$PATH +pip3 install --break-system-packages --upgrade pip +pip3 install --break-system-packages -r requirements.txt + # Go CI Lint -curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.0 +curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v1.54.0 + +# Go tools +go install golang.org/x/tools/cmd/goimports@latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8e06048..7e33c4b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -25,7 +25,7 @@ jobs: # Require: The version of golangci-lint to use. # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.53 + version: v1.57 test: runs-on: ubuntu-latest @@ -38,7 +38,7 @@ jobs: - name: 👷 Prepare the Go environment uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '1.22' - name: 🧪 Run the unit tests run: go test -v -failfast -coverprofile cover.out -timeout=1m ./... - name: 🚀 Upload the coverage reports to Codecov @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '1.22' - name: Install dependencies run: go get -d ./... @@ -72,7 +72,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '1.22' - uses: go-semantic-release/action@v1 with: hooks: goreleaser diff --git a/README.md b/README.md index 14df857..c21a8ab 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ A GO module and cli to access the ifm OVP8xx series of devices. -[![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/gomods/athens.svg)](https://github.com/gomods/athens) +[![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/graugans/go-ovp8xx.svg)](https://github.com/graugans/go-ovp8xx) +[![Go Reference](https://pkg.go.dev/badge/github.com/graugans/go-ovp8xx/v2.svg)](https://pkg.go.dev/github.com/graugans/go-ovp8xx/v2) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![example workflow](https://github.com/graugans/go-ovp8xx/actions/workflows/go.yml/badge.svg) [![codecov](https://codecov.io/gh/graugans/go-ovp8xx/graph/badge.svg?token=BU6UPYCUPI)](https://codecov.io/gh/graugans/go-ovp8xx) @@ -18,28 +19,34 @@ A GO module and cli to access the ifm OVP8xx series of devices. This project is still a work in progress and will suffer from breaking API changes. Please be warned. In case you have any suggestions or want to contribute please feel free to open an issue or pull request. -## CLI Installation +## CLI -### Pre Build Binaries +One of the benefits of the Go language is the easy way of producing statically linked binaries to be used on all major platforms. One of the `ovp8xx` core features is the CLI interface. The design tries to stay as close as possible to the XML-RPC API. + +With the CLI you can get the configuration from the device and [filter](doc/filter.md) it as you need. After transforming the config it can be written back to the device. + +### CLI Installation + +#### Pre Build Binaries The recommended and easiest way is to download the pre-build binary from the [GitHub Release page](https://github.com/graugans/go-ovp8xx/releases). ⚠️ The Windows binary maybe flagged by a Virus Scanner, please also read the note from the [Go Team](https://go.dev/doc/faq#virus) ⚠️ -### Go get +#### Go get If you have a decent Go version installed ```sh -go install github.com/graugans/go-ovp8xx/cmd/ovp8xx@latest +go install github.com/graugans/go-ovp8xx/v2/cmd/ovp8xx@latest ``` -## API usage +### API usage Within in your Go project get the ovp8xx package first ``` -go get github.com/graugans/go-ovp8xx +go get github.com/graugans/go-ovp8xx/v2 ``` The following example will query the software Version of your OVP8xx. This assumes that either the OVP8xx is using the default IP address of `192.168.0.69` or the environment variable `OVP8XX_IP` is set. In case you want to set the IP in code please use `ovp8xx.NewClient(ovp8xx.WithHost("192.168.0.69"))` to construct the client. @@ -50,7 +57,7 @@ package main import ( "fmt" - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" ) func main() { diff --git a/cmd/ovp8xx/cmd/diagnostic.go b/cmd/ovp8xx/cmd/diagnostic.go index 91fa58b..3ae0c9e 100644 --- a/cmd/ovp8xx/cmd/diagnostic.go +++ b/cmd/ovp8xx/cmd/diagnostic.go @@ -6,7 +6,7 @@ package cmd import ( "fmt" - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) diff --git a/cmd/ovp8xx/cmd/factoryReset.go b/cmd/ovp8xx/cmd/factoryReset.go index ae40222..189db54 100644 --- a/cmd/ovp8xx/cmd/factoryReset.go +++ b/cmd/ovp8xx/cmd/factoryReset.go @@ -4,7 +4,7 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) @@ -14,7 +14,7 @@ var factoryResetCmd = &cobra.Command{ Short: "Performs a factory reset of the device", Long: `Sometime one wants a fresh start. -The command factoryReset resets all settings to their defaults and erases any addtional data like Docker containers.`, +The command factoryReset resets all settings to their defaults and erases any additional data like Docker containers.`, RunE: func(cmd *cobra.Command, args []string) error { keepNetworkSettings, err := cmd.Flags().GetBool("keepnetworksettings") if err != nil { diff --git a/cmd/ovp8xx/cmd/get.go b/cmd/ovp8xx/cmd/get.go index c11cc14..cd9d19a 100644 --- a/cmd/ovp8xx/cmd/get.go +++ b/cmd/ovp8xx/cmd/get.go @@ -4,10 +4,40 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "encoding/json" + "errors" + "fmt" + "os" + "text/template" + + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) +// toJSON converts the given object to a JSON string representation. +// If an error occurs during marshaling, an empty string is returned. +// This is taken from https://github.com/intel/tfortools +func toJSON(obj interface{}) string { + b, err := json.MarshalIndent(obj, "", "\t") + if err != nil { + return "" + } + return string(b) +} + +// prefix can be used to create a list separated by s and the very first +// element is not prefixed. +func prefix(s string) func() string { + i := -1 + return func() string { + i++ + if i == 0 { + return "" + } + return s + } +} + func getCommand(cmd *cobra.Command, args []string) error { var result ovp8xx.Config var err error @@ -23,6 +53,35 @@ func getCommand(cmd *cobra.Command, args []string) error { if result, err = o3r.Get(helper.jsonPointers()); err != nil { return err } + + if cmd.Flags().Changed("format") { + if helper.prettyPrint() { + return errors.New("you can't use --pretty and --format at the same time") + } + + format, err := cmd.Flags().GetString("format") + if err != nil { + return fmt.Errorf("unable to get the format string from the command line: %w", err) + } + var inputData interface{} + if err = json.Unmarshal([]byte(result.String()), &inputData); err != nil { + return fmt.Errorf("unable to unmarshal the JSON data from the 'get' call: %w", err) + } + templateFunctions := template.FuncMap{ + "toJSON": toJSON, + "prefix": prefix, + } + tmpl, err := template.New("output").Funcs(templateFunctions).Parse(format) + if err != nil { + return fmt.Errorf("unable to parse the template: %w", err) + } + + if err := tmpl.Execute(os.Stdout, inputData); err != nil { + return fmt.Errorf("unable to execute the template: %w", err) + } + return nil + } + if err := helper.printJSONResult(result.String()); err != nil { return err } @@ -40,11 +99,11 @@ Valid queries are for example: - To query all ports including all sub elements the query "/ports" can be used. In contrast to the concept of a JSON pointer the OVP8xx does not response with the data -the pointer is pointing to, it returns the full object hirachie with the encapsulating +the pointer is pointing to, it returns the full object hierarchy with the encapsulating object paths. A query of the name of the "port6" (/ports/port6/info/name) not just returns the object of that port, -it also keeps the hirachy intact: +it also keeps the hierarchy intact: { "ports": @@ -66,4 +125,5 @@ func init() { rootCmd.AddCommand(getCmd) getCmd.Flags().StringSliceP("pointer", "p", []string{""}, "A JSON pointer to be queried") getCmd.Flags().Bool("pretty", false, "Pretty print the JSON received from the device") + getCmd.Flags().String("format", "", "Specify an alternative format for the JSON output") } diff --git a/cmd/ovp8xx/cmd/getSchema.go b/cmd/ovp8xx/cmd/getSchema.go index a5a27e8..f19da9a 100644 --- a/cmd/ovp8xx/cmd/getSchema.go +++ b/cmd/ovp8xx/cmd/getSchema.go @@ -4,7 +4,7 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) @@ -34,7 +34,7 @@ var getSchemaCmd = &cobra.Command{ Use: "getSchema", Short: "Retrieve the currently used JSON schema from the device", Long: `The OVP8xx getSchema command accepts a list of JSON pointers. -The JSON schema provides details about multiple aspects of a paramter. It +The JSON schema provides details about multiple aspects of a parameter. It contains information like the type of a parameter and its defaults. It also provides information weather a parameter is readOnly or not. @@ -76,7 +76,7 @@ The pointer '/device/swVersion/diagnostics' for example provides this informatio "type": "object" } -When no query is provided the complete schema is returend. +When no query is provided the complete schema is returned. `, RunE: getSchemaCommand, } diff --git a/cmd/ovp8xx/cmd/getinit.go b/cmd/ovp8xx/cmd/getinit.go index 7bb70a9..1a11477 100644 --- a/cmd/ovp8xx/cmd/getinit.go +++ b/cmd/ovp8xx/cmd/getinit.go @@ -4,7 +4,7 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) @@ -36,7 +36,7 @@ var getInitCmd = &cobra.Command{ Long: `The OVP8xx provides a way to store a configuration on the device NOTE: This shall be used with care, because it may lead to an system which is no -longer useable when the expectation from the safed configuration is no longer met.`, +longer useable when the expectation from the saved configuration is no longer met.`, RunE: getInitCommand, } diff --git a/cmd/ovp8xx/cmd/helper.go b/cmd/ovp8xx/cmd/helper.go index 5376563..049df35 100644 --- a/cmd/ovp8xx/cmd/helper.go +++ b/cmd/ovp8xx/cmd/helper.go @@ -17,7 +17,7 @@ type helperConfig struct { func (c *helperConfig) printJSONResult(data string) error { var message string = data - if c.pretty { + if c.prettyPrint() { var js json.RawMessage if err := json.Unmarshal([]byte(data), &js); err != nil { return errors.New("malformed json") @@ -44,6 +44,11 @@ func (c *helperConfig) remotePort() uint16 { return c.port } +// prettyPrint returns a boolean value indicating whether the output should be pretty-printed. +func (c *helperConfig) prettyPrint() bool { + return c.pretty +} + func NewHelper(cmd *cobra.Command) (helperConfig, error) { var conf = helperConfig{} var err error diff --git a/cmd/ovp8xx/cmd/pcic.go b/cmd/ovp8xx/cmd/pcic.go index 3ad989c..5c0b6d7 100644 --- a/cmd/ovp8xx/cmd/pcic.go +++ b/cmd/ovp8xx/cmd/pcic.go @@ -6,7 +6,7 @@ package cmd import ( "fmt" - "github.com/graugans/go-ovp8xx/pkg/pcic" + "github.com/graugans/go-ovp8xx/v2/pkg/pcic" "github.com/spf13/cobra" ) @@ -22,7 +22,7 @@ type PCICReceiver struct { // It takes a pcic.Frame as a parameter. func (r *PCICReceiver) Result(frame pcic.Frame) { r.frame = frame - fmt.Printf("Framecount: %d\n", r.framecount) + fmt.Printf("Frame count: %d\n", r.framecount) r.framecount++ } diff --git a/cmd/ovp8xx/cmd/reboot.go b/cmd/ovp8xx/cmd/reboot.go index 8dd17a1..d02da30 100644 --- a/cmd/ovp8xx/cmd/reboot.go +++ b/cmd/ovp8xx/cmd/reboot.go @@ -4,7 +4,7 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) diff --git a/cmd/ovp8xx/cmd/root.go b/cmd/ovp8xx/cmd/root.go index 2d2353a..b1b5435 100644 --- a/cmd/ovp8xx/cmd/root.go +++ b/cmd/ovp8xx/cmd/root.go @@ -7,10 +7,12 @@ import ( "fmt" "os" - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) +// SetVersionInfo sets the version information for the root command. +// It formats the version, commit, and date into a string and assigns it to the rootCmd.Version variable. func SetVersionInfo(version, commit, date string) { rootCmd.Version = fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit) } @@ -33,4 +35,5 @@ func Execute() { func init() { rootCmd.PersistentFlags().String("ip", ovp8xx.GetEnv("OVP8XX_IP", "192.168.0.69"), "The IP address or hostname of the OVP8XX. If not provided the default will be taken from the environment variable OVP8XX_IP") + } diff --git a/cmd/ovp8xx/cmd/saveinit.go b/cmd/ovp8xx/cmd/saveinit.go index 5cc691d..4f0366d 100644 --- a/cmd/ovp8xx/cmd/saveinit.go +++ b/cmd/ovp8xx/cmd/saveinit.go @@ -4,7 +4,7 @@ Copyright © 2023 Christian Ege package cmd import ( - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) @@ -12,11 +12,11 @@ import ( var saveInitCmd = &cobra.Command{ Use: "saveInit", Short: "Saves the init configuration on the device", - Long: `To store the configuration persistant on the device the command saveInit can be used. + Long: `To store the configuration persistent on the device the command saveInit can be used. -A safed configuration persists a reboot. This is best used in combination with the "set" command. +A saved configuration persists a reboot. This is best used in combination with the "set" command. -Please use this with care. The scope should be as narrow as posible, to prevent any conflicts. +Please use this with care. The scope should be as narrow as possible, to prevent any conflicts. In case no JSON Pointer is provided the complete configuration is saved`, RunE: func(cmd *cobra.Command, args []string) error { pointers, err := cmd.Flags().GetStringSlice("pointer") diff --git a/cmd/ovp8xx/cmd/set.go b/cmd/ovp8xx/cmd/set.go index 9fbd331..ac9528f 100644 --- a/cmd/ovp8xx/cmd/set.go +++ b/cmd/ovp8xx/cmd/set.go @@ -8,7 +8,7 @@ import ( "fmt" "io" - "github.com/graugans/go-ovp8xx/pkg/ovp8xx" + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" "github.com/spf13/cobra" ) diff --git a/cmd/ovp8xx/cmd/waitforonline.go b/cmd/ovp8xx/cmd/waitforonline.go new file mode 100644 index 0000000..7a9d0f9 --- /dev/null +++ b/cmd/ovp8xx/cmd/waitforonline.go @@ -0,0 +1,56 @@ +/* +Copyright © 2024 Christian Ege +*/ +package cmd + +import ( + "fmt" + "time" + + "github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx" + "github.com/spf13/cobra" +) + +func waitForOnlineCommand(cmd *cobra.Command, args []string) error { + var ok bool + 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 + } + + timeOutDuration := time.Duration(timeout) * time.Second + if ok, err = o3r.IsAvailable( + timeOutDuration, + ovp8xx.AndPortsAreOnline(), + ); 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/cmd/ovp8xx/ovp8xx.go b/cmd/ovp8xx/ovp8xx.go index b78a9fc..e7a34f1 100644 --- a/cmd/ovp8xx/ovp8xx.go +++ b/cmd/ovp8xx/ovp8xx.go @@ -1,7 +1,8 @@ package main import ( - "github.com/graugans/go-ovp8xx/cmd/ovp8xx/cmd" + "github.com/graugans/go-ovp8xx/v2/cmd/ovp8xx/cmd" + "github.com/graugans/go-ovp8xx/v2/internal/versioninfo" ) var ( @@ -11,6 +12,15 @@ var ( ) func main() { + // If the version is "dev", it means that the binary is built using "go install", + // "go build" or "go run". + // However, if the binary is build by Goreleaser we use that version. + if version == "dev" { + version = versioninfo.Version + commit = versioninfo.Revision + date = versioninfo.LastCommit.String() + } + cmd.SetVersionInfo( version, commit, diff --git a/doc/filter.md b/doc/filter.md new file mode 100644 index 0000000..f67a1d9 --- /dev/null +++ b/doc/filter.md @@ -0,0 +1,92 @@ +# Using Go text/template for Advanced Data Filtering + +This document demonstrates the usage of the Go text/template package to filter and format the output of the `ovp8xx get` command for manipulating the result data. There are two custom functions created to enhance the filtering: `prefix` and `toJSON`. + +## The `prefix` Function + +The `prefix` function is designed to solve the problem of trailing commas, which are, for example, not allowed in JSON. The idea is to prefix each line with a comma, except for the very first one. This function is particularly useful when generating JSON output dynamically, where the number of elements may vary. + +Here's how it's used in the command: + +```sh +ovp8xx get --format '{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{call $p}}"{{- $key -}}":{"state": "{{ .state }}"}{{- end }} } }' +``` + +In this command, `{{$p := prefix ", "}}` initializes the prefix function with a comma and a space. Then, `{{call $p}}` is used to insert the prefix before each port entry. The prefix function ensures that no comma is inserted before the first entry. + +The result is a JSON object where each line, except the first, is prefixed with a comma: + +```sh +{ + "ports": { + "port0": { + "state": "RUN" + }, + "port1": { + "state": "RUN" + }, + "port2": { + "state": "RUN" + }, + "port3": { + "state": "RUN" + }, + "port6": { + "state": "RUN" + } + } +} +``` + +## Print all ports with some details + +The following command retrieves all port data and formats it for easy reading: + +```sh +ovp8xx get --format '{{$p := prefix "\n"}}{{ range $port, $details := .ports }}{{call $p}}[{{ $port }}] state: {{ $details.state }},{{print "\t"}}type: {{ $details.info.features.type }},{{print "\t"}}PCIC Port: {{ $details.data.pcicTCPPort }}{{ end }}' +``` + +The result will look like: + +``` +[port0] state: RUN, type: 3D, PCIC Port: 50010 +[port1] state: RUN, type: 3D, PCIC Port: 50011 +[port2] state: RUN, type: 2D, PCIC Port: 50012 +[port3] state: RUN, type: 2D, PCIC Port: 50013 +[port6] state: RUN, type: IMU, PCIC Port: 50016 +``` + +## Modify the state of the ports + +Now we are taking the example from the [`prefix`](#the-prefix-function) function and enhance it to also modify the state of all ports (except port6) to CONF with the following command: + +```sh +ovp8xx get --format '{{ $state := "CONF" }}{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{ if ne $key "port6" }}{{call $p}}"{{- $key -}}":{"state": "{{ $state }}"}{{end}}{{- end }} } }' +``` + +The output will be a JSON object that can be directly piped into the set command: + +```sh +{ + "ports": { + "port0": { + "state": "CONF" + }, + "port1": { + "state": "CONF" + }, + "port2": { + "state": "CONF" + }, + "port3": { + "state": "CONF" + } + } +} +``` + +To directly apply those changes to the device, use the following command: + +```sh +ovp8xx get --format '{{ $state := "CONF" }}{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{ if ne $key "port6" }}{{call $p}}"{{- $key -}}":{"state": "{{ $state }}"}{{end}}{{- end }} } }' | ovp8xx set +``` diff --git a/go.mod b/go.mod index 2e7859e..444eb3b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/graugans/go-ovp8xx +module github.com/graugans/go-ovp8xx/v2 go 1.21 diff --git a/internal/versioninfo/version.go b/internal/versioninfo/version.go new file mode 100644 index 0000000..63335e9 --- /dev/null +++ b/internal/versioninfo/version.go @@ -0,0 +1,46 @@ +// Package versioninfo uses runtime.ReadBuildInfo() to set global executable revision information if possible. +package versioninfo + +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 Carl Johnson +// Based on https://github.com/earthboundkid/versioninfo + +import ( + "runtime/debug" + "time" +) + +var ( + // Version will be the version tag if the binary is built with "go install url/tool@version". + // If the binary is built some other way, it will be "(devel)". + Version = "unknown" + // Revision is taken from the vcs.revision tag in Go 1.18+. + Revision = "unknown" + // LastCommit is taken from the vcs.time tag in Go 1.18+. + LastCommit time.Time + // DirtyBuild is taken from the vcs.modified tag in Go 1.18+. + DirtyBuild = true +) + +func init() { + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + for _, kv := range info.Settings { + if kv.Value == "" { + continue + } + switch kv.Key { + case "vcs.revision": + Revision = kv.Value + case "vcs.time": + LastCommit, _ = time.Parse(time.RFC3339, kv.Value) + case "vcs.modified": + DirtyBuild = kv.Value == "true" + } + } + if info.Main.Version != "" { + Version = info.Main.Version + } +} diff --git a/pkg/ovp8xx/client.go b/pkg/ovp8xx/client.go index d8497f4..1854674 100644 --- a/pkg/ovp8xx/client.go +++ b/pkg/ovp8xx/client.go @@ -1,7 +1,11 @@ package ovp8xx import ( + "context" + "encoding/json" "fmt" + "slices" + "time" ) type ( @@ -16,6 +20,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(WithHost("192.168.47.11")) +// // ... func NewClient(opts ...ClientOption) *Client { // Initialise with default values client := &Client{ @@ -30,15 +41,104 @@ 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 } + +// WaitForConfig represents the configuration for waiting for a specific stage. +type WaitForConfig struct { + stage string // The stage to wait for. +} + +// WaitForOption is a function type that is used as an option for configuring the behavior of the WaitFor function. +// It takes a pointer to a WaitForConfig struct as a parameter and can be used to modify its properties. +type WaitForOption func(c *WaitForConfig) + +// AndPortsAreOnline returns a WaitForOption function that sets the stage to "ports". +func AndPortsAreOnline() WaitForOption { + return func(w *WaitForConfig) { + w.stage = "ports" + } +} + +// AndAppsAreOnline returns a WaitForOption function that sets the stage to "applications". +func AndAppsAreOnline() WaitForOption { + return func(w *WaitForConfig) { + w.stage = "applications" + } +} + +// 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. +// Example usage: +// +// ok, err := device.IsAvailable(time.Duration(timeout) * time.Second, ovp8xx.AndPortsAreOnline()) +// // ... +func (d *Client) IsAvailable(timeout time.Duration, opts ...WaitForOption) (bool, error) { + var err error + proc := make(chan struct{}, 1) + conf := *NewConfig() + + waitingFor := &WaitForConfig{ + stage: "device", + } + + // Apply wait options + for _, opt := range opts { + opt(waitingFor) + } + + // result is a struct that represents the response from the OVP8xx device. + // It contains information about the device's diagnostic data, such as the configuration initialization stages. + result := struct { + Device struct { + Diagnostic struct { + ConfInitStages []string `json:"confInitStages"` + } `json:"diagnostic"` + } `json:"device"` + }{} + + go func() { + for { + if conf, err = d.Get([]string{"/device/diagnostic/confInitStages"}); err != nil { + // In case of an error retry, regardless of an timeout + continue + } + // Unmarshal the data into the result struct + if err = json.Unmarshal([]byte(conf.String()), &result); err != nil { + // In case of an error retry until the timeout + continue + } + // Check if the device is ready + if slices.Contains(result.Device.Diagnostic.ConfInitStages, waitingFor.stage) { + // 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..e4604ef 100644 --- a/pkg/ovp8xx/rpc.go +++ b/pkg/ovp8xx/rpc.go @@ -1,9 +1,29 @@ 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 +46,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 +59,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 +82,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 +100,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 +124,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 +146,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 +171,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 +195,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 { diff --git a/pkg/pcic/chunk_test.go b/pkg/pcic/chunk_test.go index 7636f22..e2c9008 100644 --- a/pkg/pcic/chunk_test.go +++ b/pkg/pcic/chunk_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/graugans/go-ovp8xx/pkg/pcic" + "github.com/graugans/go-ovp8xx/v2/pkg/pcic" "github.com/stretchr/testify/assert" ) diff --git a/pkg/pcic/protocol.go b/pkg/pcic/protocol.go index 0bec80c..7ef1038 100644 --- a/pkg/pcic/protocol.go +++ b/pkg/pcic/protocol.go @@ -160,7 +160,6 @@ func errorParser(data []byte) (ErrorMessage, error) { } func asyncResultParser(data []byte) (Frame, error) { - fmt.Printf("Async Data received\n") frame := Frame{} var err error contentDecorated := data[:len(data)-delimiterFieldLength] diff --git a/pkg/pcic/protocol_test.go b/pkg/pcic/protocol_test.go index 06c0fcc..c3d3ef6 100644 --- a/pkg/pcic/protocol_test.go +++ b/pkg/pcic/protocol_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/graugans/go-ovp8xx/pkg/pcic" + "github.com/graugans/go-ovp8xx/v2/pkg/pcic" "github.com/stretchr/testify/assert" )