├── docs ├── CNAME ├── stylesheets │ └── extra.css ├── cmd │ ├── flush.md │ ├── get.md │ └── modify.md ├── install.md ├── index.md └── user_guide.md ├── .gribic.yaml ├── app ├── types.go ├── utils.go ├── target.go ├── version.go ├── flush.go ├── get.go ├── app.go ├── modify.go └── workflow.go ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── release.yml ├── goreleaser.dockerfile ├── cmd ├── version.go ├── get.go ├── flush.go ├── modify.go ├── workflow.go ├── versionUpgrade.go └── root.go ├── .gitignore ├── Dockerfile ├── README.md ├── main.go ├── api ├── flush.go ├── api.go ├── get.go ├── modify.go └── entry.go ├── examples ├── operations │ ├── delete_oper.yaml │ ├── add_oper_single_primary.yaml │ └── add_oper_all_primary.yaml └── workflow │ └── workflow1.yaml ├── .goreleaser.yaml ├── config ├── gnmi_server.go ├── workflow.go ├── config.go ├── modify_test.go ├── target.go └── modify.go ├── mkdocs.yml ├── go.mod └── install.sh /docs/CNAME: -------------------------------------------------------------------------------- 1 | gribic.kmrd.dev 2 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .md-typeset code { 2 | background-color: transparent ; 3 | } -------------------------------------------------------------------------------- /.gribic.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | clab-gribi-srl1: 3 | username: admin 4 | password: NokiaSrl1! 5 | skip-verify: true 6 | clab-gribi-srl2: 7 | username: admin 8 | password: NokiaSrl1! 9 | skip-verify: true 10 | -------------------------------------------------------------------------------- /app/types.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "time" 4 | 5 | const ( 6 | defaultGrpcPort = "57401" 7 | msgSize = 512 * 1024 * 1024 8 | defaultRetryTimer = 10 * time.Second 9 | ) 10 | 11 | type TargetError struct { 12 | TargetName string 13 | Err error 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /goreleaser.dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | LABEL maintainer="Karim Radhouani " 4 | LABEL documentation="https://gribic.kmrd.dev" 5 | LABEL repo="https://github.com/karimra/gribic" 6 | 7 | COPY gribic /app/gribic 8 | ENTRYPOINT [ "/app/gribic" ] 9 | CMD [ "help" ] 10 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - run: docker run -v $(pwd):/docs --entrypoint mkdocs squidfunk/mkdocs-material:7.1.0 gh-deploy --force --strict -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | // newVersionCmd represents the version command 6 | func newVersionCmd() *cobra.Command { 7 | cmd := &cobra.Command{ 8 | Use: "version", 9 | Short: "print gRIBIc version", 10 | RunE: gApp.RunEVersion, 11 | SilenceUsage: true, 12 | } 13 | return cmd 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | tests 18 | dist 19 | gribic -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19.5 as builder 2 | ADD . /build 3 | WORKDIR /build 4 | RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o gribic . 5 | 6 | FROM alpine 7 | 8 | LABEL maintainer="Karim Radhouani " 9 | LABEL documentation="https://gribic.kmrd.dev" 10 | LABEL repo="https://github.com/karimra/gribic" 11 | COPY --from=builder /build/gribic /app/ 12 | ENTRYPOINT [ "/app/gribic" ] 13 | CMD [ "help" ] 14 | -------------------------------------------------------------------------------- /cmd/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Karim Radhouani 3 | 4 | 5 | */ 6 | package cmd 7 | 8 | import ( 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newGetCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "get", 15 | Aliases: []string{"g"}, 16 | Short: "run gRIBI Get RPC", 17 | 18 | RunE: gApp.GetRunE, 19 | SilenceUsage: true, 20 | } 21 | // init flags 22 | gApp.InitGetFlags(cmd) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/flush.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Karim Radhouani 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func newFlushCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "flush", 14 | Aliases: []string{"f"}, 15 | Short: "run gRIBI Flush RPC", 16 | PreRunE: gApp.FlushPreRunE, 17 | RunE: gApp.FlushRunE, 18 | SilenceUsage: true, 19 | } 20 | // init flags 21 | gApp.InitFlushFlags(cmd) 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /cmd/modify.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Karim Radhouani 3 | 4 | 5 | */ 6 | package cmd 7 | 8 | import ( 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newModifyCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "modify", 15 | Aliases: []string{"mod", "m"}, 16 | Short: "run gRIBI Modify RPC", 17 | PreRunE: gApp.ModifyPreRunE, 18 | RunE: gApp.ModifyRunE, 19 | SilenceUsage: true, 20 | } 21 | gApp.InitModifyFlags(cmd) 22 | // init flags 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/workflow.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func newWorkflowCmd() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "workflow", 13 | Aliases: []string{"wf", "w"}, 14 | Short: "run a workflow", 15 | PreRun: func(cmd *cobra.Command, _ []string) { 16 | gApp.Config.SetLocalFlagsFromFile(cmd) 17 | }, 18 | PreRunE: gApp.WorkflowPreRunE, 19 | RunE: gApp.WorkflowRunE, 20 | SilenceUsage: true, 21 | } 22 | // init flags 23 | gApp.InitWorkflowFlags(cmd) 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `gribic` is a gRIBI CLI client that provides full support for gRIBI RPCs. 2 | It is intended to be used for educational and testing purposes. 3 | 4 | ## Features 5 | 6 | * **Full Support for Get And Flush RPCs** 7 | 8 | * **Modify RPC is supported with IPv4, IPv6, Next Hop Group and Next Hop AFTs** 9 | 10 | * **Template based modify RPC operations configuration** 11 | 12 | * **Concurrent multi target RPC execution** 13 | 14 | Documentation available at [https://gribic.kmrd.dev](https://gribic.kmrd.dev) 15 | 16 | ## Quick start guide 17 | 18 | ### Installation 19 | 20 | ```bash 21 | bash -c "$(curl -sL https://get-gribic.kmrd.dev)" 22 | ``` 23 | -------------------------------------------------------------------------------- /app/utils.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | func (a *App) handleErrs(errs []error) error { 11 | numErrors := len(errs) 12 | if numErrors > 0 { 13 | for _, e := range errs { 14 | a.Logger.Debug(e) 15 | } 16 | return fmt.Errorf("there was %d error(s)", numErrors) 17 | } 18 | return nil 19 | } 20 | 21 | func flagIsSet(cmd *cobra.Command, name string) bool { 22 | if cmd == nil { 23 | return false 24 | } 25 | var isSet bool 26 | cmd.Flags().VisitAll(func(f *pflag.Flag) { 27 | if f.Name == name && f.Changed { 28 | isSet = true 29 | return 30 | } 31 | }) 32 | return isSet 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Karim Radhouani 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import "github.com/karimra/gribic/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /api/flush.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | spb "github.com/openconfig/gribi/v1/proto/service" 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | func NewFlushRequest(opts ...GRIBIOption) (*spb.FlushRequest, error) { 11 | m := new(spb.FlushRequest) 12 | err := apply(m, opts...) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return m, nil 17 | } 18 | 19 | func Override() func(m proto.Message) error { 20 | return func(msg proto.Message) error { 21 | if msg == nil { 22 | return ErrInvalidMsgType 23 | } 24 | switch msg := msg.ProtoReflect().Interface().(type) { 25 | case *spb.FlushRequest: 26 | msg.Election = &spb.FlushRequest_Override{} 27 | default: 28 | return fmt.Errorf("option Override: %w: %T", ErrInvalidMsgType, msg) 29 | } 30 | return nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | 6 | "google.golang.org/protobuf/proto" 7 | ) 8 | 9 | type GRIBIOption func(proto.Message) error 10 | 11 | // ErrInvalidMsgType is returned by a GRIBIOption in case the Option is supplied 12 | // an unexpected proto.Message 13 | var ErrInvalidMsgType = errors.New("invalid message type") 14 | 15 | // ErrInvalidValue is returned by a GRIBIOption in case the Option is supplied 16 | // an unexpected value. 17 | var ErrInvalidValue = errors.New("invalid value") 18 | 19 | // apply is a helper function that simply applies the options to the proto.Message. 20 | // It returns an error if any of the options fails. 21 | func apply(m proto.Message, opts ...GRIBIOption) error { 22 | for _, o := range opts { 23 | if err := o(m); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /cmd/versionUpgrade.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | // upgradeCmd represents the version command 11 | func newVersionUpgradeCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "upgrade", 14 | Aliases: []string{"up"}, 15 | Short: "upgrade gRIBIc to latest available version", 16 | PreRun: func(cmd *cobra.Command, _ []string) { 17 | gApp.Config.SetLocalFlagsFromFile(cmd) 18 | }, 19 | RunE: gApp.VersionUpgradeRun, 20 | } 21 | initVersionUpgradeFlags(cmd) 22 | return cmd 23 | } 24 | 25 | func initVersionUpgradeFlags(cmd *cobra.Command) { 26 | cmd.Flags().Bool("use-pkg", false, "upgrade using package") 27 | cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { 28 | gApp.Config.FileConfig.BindPFlag(fmt.Sprintf("%s-%s", cmd.Name(), flag.Name), flag) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | env: 9 | GOVER: 1.21.4 10 | GORELEASER_VER: v1.19.2 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ env.GOVER }} 20 | - run: go test -cover ./... 21 | env: 22 | CGO_ENABLED: 0 23 | 24 | release: 25 | runs-on: ubuntu-22.04 26 | needs: 27 | - test 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ env.GOVER }} 33 | 34 | - name: Login to github container registry 35 | run: docker login ghcr.io -u karimra -p ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Release with goreleaser 38 | uses: goreleaser/goreleaser-action@v5 39 | with: 40 | version: ${{ env.GORELEASER_VER }} 41 | args: release --clean -f .goreleaser.yaml 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /docs/cmd/flush.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | The Flush Command runs a [gRIBI Flush RPC](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L47) as a client, sending a [FlushRequest](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L469) to a gRIBI server. 4 | The Server returns a single [FlushResponse](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L518). 5 | 6 | ### Usage 7 | 8 | `gribic [global-flags] flush [local-flags]` 9 | 10 | Alias: `f` 11 | 12 | ### Flags 13 | 14 | #### ns 15 | 16 | The `--ns` flag sets the network instance name the client wants to flush. 17 | 18 | #### ns-all 19 | 20 | The `--ns-all` flag indicates to the server that the client wants to flush all instances. 21 | 22 | #### override 23 | 24 | The `--override` flag indicates to the server that the client wants the server to not compare the election ID with already known `single-primary` clients. 25 | 26 | ### Examples 27 | 28 | Flush all AFTs in network instance `default` 29 | 30 | ```bash 31 | gribic -a router1 -u admin -p admin --skip-verify flush --ns default 32 | ``` 33 | 34 | Flush all AFTs in all network instances 35 | 36 | ```bash 37 | gribic -a router1 -u admin -p admin --skip-verify flush --ns-all 38 | ``` 39 | -------------------------------------------------------------------------------- /examples/operations/delete_oper.yaml: -------------------------------------------------------------------------------- 1 | # the network instance name to be used if none is 2 | # set under an operation configuration. 3 | default-network-instance: default 4 | 5 | params: 6 | redundancy: single-primary 7 | persistence: preserve 8 | ack-type: fib 9 | 10 | # list of operations to send towards targets, 11 | # only NH, NHG and IPv4 are supported 12 | operations: 13 | - op: delete 14 | # network-instance: not_default 15 | ipv4: 16 | prefix: 1.1.1.0/24 17 | nhg: 1 18 | nhg-network-instance: default 19 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 20 | # entry-metadata: # string 21 | - op: delete 22 | nhg: 23 | id: 1 24 | # backup-nhg: # uint 25 | # color: # uint 26 | next-hop: 27 | - index: 1 28 | # weight: 1 # uint 29 | # programmed-id: # uint 30 | - op: delete 31 | # network-instance: # 32 | # election-id: # 33 | nh: 34 | index: 1 35 | ip-address: 192.168.1.2 36 | # interface-reference: 37 | # interface: 38 | # subinterface: 39 | # ip-in-ip: 40 | # dst-ip: 41 | # src-ip: 42 | # mac: 43 | # network-instance: 44 | # programmed-index: 45 | # pushed-mpls-label-stack: 46 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 47 | # label: # uint 48 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/karimra/gribic/app" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var gApp = app.New() 13 | 14 | func newRootCmd() *cobra.Command { 15 | gApp.RootCmd = &cobra.Command{ 16 | Use: "gribic", 17 | Short: "run gRIBI RPCs from the terminal", 18 | PreRun: func(cmd *cobra.Command, _ []string) { 19 | gApp.Config.SetPersistentFlagsFromFile(cmd) 20 | }, 21 | PersistentPreRunE: gApp.PreRun, 22 | } 23 | gApp.InitGlobalFlags() 24 | // 25 | versionCmd := newVersionCmd() 26 | versionCmd.AddCommand(newVersionUpgradeCmd()) 27 | gApp.RootCmd.AddCommand( 28 | versionCmd, 29 | newGetCmd(), 30 | newModifyCmd(), 31 | newFlushCmd(), 32 | newWorkflowCmd(), 33 | ) 34 | return gApp.RootCmd 35 | } 36 | 37 | // Execute adds all child commands to the root command and sets flags appropriately. 38 | // This is called by main.main(). It only needs to happen once to the rootCmd. 39 | func Execute() { 40 | err := newRootCmd().Execute() 41 | if err != nil { 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func init() { 47 | cobra.OnInitialize(initConfig) 48 | } 49 | 50 | // initConfig reads in config file and ENV variables if set. 51 | func initConfig() { 52 | err := gApp.Config.Load(gApp.Context()) 53 | if err == nil { 54 | return 55 | } 56 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 57 | fmt.Fprintf(os.Stderr, "failed loading config file: %v\n", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/target.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/karimra/gribic/config" 7 | spb "github.com/openconfig/gribi/v1/proto/service" 8 | "github.com/openconfig/gribigo/rib" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | type target struct { 13 | // configuration 14 | Config *config.TargetConfig 15 | // gRPC connection 16 | conn *grpc.ClientConn 17 | // gRIBI client 18 | gRIBIClient spb.GRIBIClient 19 | // modify stream client 20 | modClient spb.GRIBI_ModifyClient 21 | // cancel function 22 | cfn context.CancelFunc 23 | // modify stream cancel function 24 | modifyCfn context.CancelFunc 25 | // RIB 26 | rib *rib.RIB 27 | } 28 | 29 | func NewTarget(tc *config.TargetConfig) *target { 30 | return &target{ 31 | Config: tc, 32 | rib: rib.New(tc.DefaultNI), 33 | } 34 | } 35 | 36 | func (a *App) GetTargets() (map[string]*target, error) { 37 | targetsConfigs, err := a.Config.GetTargets() 38 | if err != nil { 39 | return nil, err 40 | } 41 | targets := make(map[string]*target) 42 | for n, tc := range targetsConfigs { 43 | targets[n] = NewTarget(tc) 44 | } 45 | return targets, nil 46 | } 47 | 48 | func (t *target) Close() error { 49 | if t.conn == nil { 50 | return nil 51 | } 52 | return t.conn.Close() 53 | } 54 | 55 | func (t *target) createModifyClient(ctx context.Context) error { 56 | mctx, cancel := context.WithCancel(ctx) 57 | t.modifyCfn = cancel 58 | var err error 59 | t.modClient, err = t.gRIBIClient.Modify(appendCredentials(mctx, t.Config)) 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | `gribic` is a single binary built for the Linux, Mac OS and Windows platforms distributed via [Github releases](https://github.com/karimra/gribic/releases). 4 | 5 | ### Linux/Mac OS 6 | 7 | To download & install the latest release the following automated [installation script](https://github.com/karimra/gribic/blob/main/install.sh) can be used: 8 | 9 | ```bash 10 | bash -c "$(curl -sL https://get-gribic.kmrd.dev)" 11 | ``` 12 | 13 | As a result, the latest `gribic` version will be installed in the `/usr/local/bin` directory and the version information will be printed out. 14 | 15 | ```text 16 | Downloading https://github.com/karimra/gribic/releases/download/v0.0.1/gribic_0.0.1_linux_x86_64.tar.gz 17 | Preparing to install gribic 0.0.1 into /usr/local/bin 18 | gribic installed into /usr/local/bin/gribic 19 | version : 0.0.1 20 | commit : 90c5d07 21 | date : 2022-03-13T19:05:43Z 22 | gitURL : https://github.com/karimra/gribic 23 | docs : https://gribic.kmrd.dev 24 | ``` 25 | 26 | #### Packages 27 | 28 | Linux users running distributions with support for `deb`/`rpm` packages can install `gribic` using pre-built packages: 29 | 30 | ```bash 31 | bash -c "$(curl -sL https://get-gribic.kmrd.dev)" -- --use-pkg 32 | ``` 33 | 34 | ### Docker 35 | 36 | The `gribic` container image can be pulled from GitHub container registries. The tag of the image corresponds to the release version and `latest` tag points to the latest available release: 37 | 38 | ```bash 39 | # pull latest release from github registry 40 | docker pull ghcr.io/karimra/gribic:latest 41 | # pull a specific release from github registry 42 | docker pull ghcr.io/karimra/gribic:0.0.1 43 | ``` 44 | 45 | ```bash 46 | docker run \ 47 | --network host \ 48 | --rm ghcr.io/karimra/gribic --help 49 | ``` 50 | -------------------------------------------------------------------------------- /app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "time" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | version = "dev" 16 | commit = "none" 17 | date = "unknown" 18 | gitURL = "" 19 | downloadURL = "https://github.com/karimra/gribic/raw/main/install.sh" 20 | ) 21 | 22 | func (a *App) RunEVersion(cmd *cobra.Command, args []string) error { 23 | fmt.Printf("version : %s\n", version) 24 | fmt.Printf(" commit : %s\n", commit) 25 | fmt.Printf(" date : %s\n", date) 26 | fmt.Printf(" gitURL : %s\n", gitURL) 27 | fmt.Printf(" docs : https://gribic.kmrd.dev\n") 28 | return nil 29 | } 30 | 31 | func (a *App) VersionUpgradeRun(cmd *cobra.Command, args []string) error { 32 | f, err := os.CreateTemp("", "gribic") 33 | defer os.Remove(f.Name()) 34 | if err != nil { 35 | return err 36 | } 37 | err = downloadFile(downloadURL, f) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | var c *exec.Cmd 43 | switch a.Config.LocalFlags.UpgradeUsePkg { 44 | case true: 45 | c = exec.Command("bash", f.Name(), "--use-pkg") 46 | case false: 47 | c = exec.Command("bash", f.Name()) 48 | } 49 | 50 | c.Stdout = os.Stdout 51 | c.Stderr = os.Stderr 52 | err = c.Run() 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | // downloadFile will download a file from a URL and write its content to a file 60 | func downloadFile(url string, file *os.File) error { 61 | client := http.Client{Timeout: 10 * time.Second} 62 | // Get the data 63 | resp, err := client.Get(url) 64 | if err != nil { 65 | return err 66 | } 67 | defer resp.Body.Close() 68 | 69 | // Write the body to file 70 | _, err = io.Copy(file, resp.Body) 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /docs/cmd/get.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | The Get Command runs a [gRIBI Get RPC](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L42) as a client, sending a [GetRequest](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L422) to a gRIBI server. 4 | The Server returns a stream of [GetResponse(s)](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L462) with the installed [AFTs](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L444). 5 | 6 | The client can specify the type of AFTs as well as the network instance it is interested on. Or simply request ALL AFT types from ALL network instances. 7 | 8 | ### Usage 9 | 10 | `gribic [global-flags] get [local-flags]` 11 | 12 | Alias: `g` 13 | 14 | ### Flags 15 | 16 | #### ns 17 | 18 | The `--ns` flag sets the network instance name the client is interested on. 19 | 20 | #### aft 21 | 22 | The `--aft` flag sets the AFT type the client is interested on. It defaults to `ALL` which means all AFT types. 23 | 24 | Accepted values: 25 | 26 | - `all` 27 | - `nexthop` (or `nh`) 28 | - `nexthop-group` (or `nhg`) 29 | - `ipv4` 30 | - `ipv6` 31 | - `mac` 32 | - `mpls` 33 | - `policy-forwarding` (or `pf`) 34 | 35 | ### Examples 36 | 37 | Query all AFTs in network instance `default` 38 | 39 | ```bash 40 | gribic -a router1 -u admin -p admin --skip-verify get --ns default 41 | ``` 42 | 43 | Query all AFTs in all network instances 44 | 45 | ```bash 46 | gribic -a router1 -u admin -p admin --skip-verify get --ns-all 47 | ``` 48 | 49 | Query AFT type `ipv4` in network instance `default` 50 | 51 | ```bash 52 | gribic -a router1 -u admin -p admin --skip-verify get --ns default --aft ipv4 53 | ``` 54 | 55 | Query AFT type `nhg` (next hop group) in all network instances 56 | 57 | ```bash 58 | gribic -a router1 -u admin -p admin --skip-verify get --ns-all --aft nhg 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to gRIBIc 2 | 3 | `gRIBIc` is a gRIBI CLI client that implements the Openconfig gRIBI RPCs. 4 | It is intended to be used for educational and testing purposes. 5 | 6 | ## Features 7 | 8 | * **Full Support for Get And Flush RPCs** 9 | 10 | * **Modify RPC is supported with IPv4, IPv6, Next Hop Group and Next Hop AFTs** 11 | 12 | * **Template based modify RPC operations configuration** 13 | 14 | * **Concurrent multi target RPC execution** 15 | 16 | ## Quick start guide 17 | 18 | ### Installation 19 | 20 | ``` 21 | bash -c "$(curl -sL https://get-gribic.kmrd.dev)" 22 | ``` 23 | 24 | ### Get Request 25 | 26 | Query all AFTs in all network instances 27 | 28 | ```bash 29 | gribic -a router1 -u admin -p admin --skip-verify get 30 | ``` 31 | 32 | Query all AFTs in network instance `default` 33 | 34 | ```bash 35 | gribic -a router1 -u admin -p admin --skip-verify get --ns default 36 | ``` 37 | 38 | Query AFT type `ipv4` in network instance `default` 39 | 40 | ```bash 41 | gribic -a router1 -u admin -p admin --skip-verify get --ns default --aft ipv4 42 | ``` 43 | 44 | Query AFT type `nhg` (next hop group) in all network instances 45 | 46 | ```bash 47 | gribic -a router1 -u admin -p admin --skip-verify get --aft nhg 48 | ``` 49 | 50 | ### Flush Request 51 | 52 | Flush all AFTs in network instance `default` 53 | 54 | ```bash 55 | gribic -a router1 -u admin -p admin --skip-verify flush --ns default 56 | ``` 57 | 58 | Flush all AFTs in all network instances 59 | 60 | ```bash 61 | gribic -a router1 -u admin -p admin --skip-verify flush --ns-all 62 | ``` 63 | 64 | ### Modify Request 65 | 66 | Run all operations defined in the input-file in `single-primary` redundancy mode, with persistence `preserve` and ack mode `RIB_FIB` 67 | 68 | ```bash 69 | gribic -a router1 -u admin -p admin --skip-verify modify \ 70 | --single-primary \ 71 | --preserve \ 72 | --fib \ 73 | --election-id 1:2 \ 74 | --input-file 75 | ``` 76 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: gribic 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | ldflags: 6 | - -s -w -X github.com/karimra/gribic/app.version={{.Version}} -X github.com/karimra/gribic/app.commit={{.ShortCommit}} -X github.com/karimra/gribic/app.date={{.Date}} -X github.com/karimra/gribic/app.gitURL={{.GitURL}} 7 | goos: 8 | - linux 9 | - darwin 10 | goarch: 11 | - amd64 12 | - "386" 13 | - arm 14 | - arm64 15 | dockers: 16 | - goos: linux 17 | goarch: amd64 18 | ids: 19 | - gribic 20 | image_templates: 21 | - "ghcr.io/karimra/gribic:latest" 22 | - 'ghcr.io/karimra/gribic:{{ replace .Version "v" ""}}' 23 | dockerfile: goreleaser.dockerfile 24 | skip_push: false 25 | archives: 26 | - name_template: >- 27 | {{ .ProjectName }}_ 28 | {{- .Version }}_ 29 | {{- title .Os }}_ 30 | {{- if eq .Arch "amd64" }}x86_64 31 | {{- else if eq .Arch "386" }}i386 32 | {{- else if eq .Arch "arm" }}armv7 33 | {{- else if eq .Arch "arm64" }}aarch64 34 | {{- else }}{{ .Arch }}{{ end }} 35 | checksum: 36 | name_template: "checksums.txt" 37 | snapshot: 38 | name_template: "{{ .Tag }}" 39 | changelog: 40 | sort: asc 41 | filters: 42 | exclude: 43 | - "^docs:" 44 | - "^test:" 45 | 46 | nfpms: 47 | - id: gribic 48 | file_name_template: >- 49 | {{ .ProjectName }}_ 50 | {{- .Version }}_ 51 | {{- title .Os }}_ 52 | {{- if eq .Arch "amd64" }}x86_64 53 | {{- else if eq .Arch "386" }}i386 54 | {{- else if eq .Arch "arm" }}armv7 55 | {{- else if eq .Arch "arm64" }}aarch64 56 | {{- else }}{{ .Arch }}{{ end }} 57 | vendor: gribic 58 | homepage: https://gribic.kmrd.dev 59 | maintainer: Karim Radhouani 60 | description: Openconfig gRIBI client implementation 61 | license: Apache 2.0 62 | formats: 63 | - deb 64 | - rpm 65 | bindir: /usr/local/bin 66 | -------------------------------------------------------------------------------- /config/gnmi_server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | const ( 9 | defaultAddress = ":57400" 10 | defaultMaxUnaryRPC = 64 11 | ) 12 | 13 | type gnmiServer struct { 14 | Address string `mapstructure:"address,omitempty" json:"address,omitempty"` 15 | // TLS 16 | SkipVerify bool `mapstructure:"skip-verify,omitempty" json:"skip-verify,omitempty"` 17 | CaFile string `mapstructure:"ca-file,omitempty" json:"ca-file,omitempty"` 18 | CertFile string `mapstructure:"cert-file,omitempty" json:"cert-file,omitempty"` 19 | KeyFile string `mapstructure:"key-file,omitempty" json:"key-file,omitempty"` 20 | // 21 | MaxUnaryRPC int64 `mapstructure:"max-unary-rpc,omitempty" json:"max-unary-rpc,omitempty"` 22 | EnableMetrics bool `mapstructure:"enable-metrics,omitempty" json:"enable-metrics,omitempty"` 23 | Debug bool `mapstructure:"debug,omitempty" json:"debug,omitempty"` 24 | } 25 | 26 | func (c *Config) GetGNMIServer() error { 27 | if !c.FileConfig.IsSet("gnmi-server") { 28 | return nil 29 | } 30 | c.GnmiServer = new(gnmiServer) 31 | c.GnmiServer.Address = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/address")) 32 | 33 | maxRPCVal := os.ExpandEnv(c.FileConfig.GetString("gnmi-server/max-unary-rpc")) 34 | if maxRPCVal != "" { 35 | maxUnaryRPC, err := strconv.Atoi(os.ExpandEnv(c.FileConfig.GetString("gnmi-server/max-unary-rpc"))) 36 | if err != nil { 37 | return err 38 | } 39 | c.GnmiServer.MaxUnaryRPC = int64(maxUnaryRPC) 40 | } 41 | 42 | c.GnmiServer.SkipVerify = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/skip-verify")) == "true" 43 | c.GnmiServer.CaFile = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/ca-file")) 44 | c.GnmiServer.CertFile = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/cert-file")) 45 | c.GnmiServer.KeyFile = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/key-file")) 46 | 47 | c.GnmiServer.EnableMetrics = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/enable-metrics")) == "true" 48 | c.GnmiServer.Debug = os.ExpandEnv(c.FileConfig.GetString("gnmi-server/debug")) == "true" 49 | c.setGnmiServerDefaults() 50 | 51 | return nil 52 | } 53 | 54 | func (c *Config) setGnmiServerDefaults() { 55 | if c.GnmiServer.Address == "" { 56 | c.GnmiServer.Address = defaultAddress 57 | } 58 | if c.GnmiServer.MaxUnaryRPC <= 0 { 59 | c.GnmiServer.MaxUnaryRPC = defaultMaxUnaryRPC 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /api/get.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | spb "github.com/openconfig/gribi/v1/proto/service" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | func NewGetRequest(opts ...GRIBIOption) (*spb.GetRequest, error) { 12 | m := new(spb.GetRequest) 13 | err := apply(m, opts...) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return m, nil 18 | } 19 | 20 | func NSAll() func(m proto.Message) error { 21 | return func(msg proto.Message) error { 22 | if msg == nil { 23 | return ErrInvalidMsgType 24 | } 25 | switch msg := msg.ProtoReflect().Interface().(type) { 26 | case *spb.GetRequest: 27 | msg.NetworkInstance = &spb.GetRequest_All{} 28 | case *spb.FlushRequest: 29 | msg.NetworkInstance = &spb.FlushRequest_All{} 30 | default: 31 | return fmt.Errorf("option NSAll: %w: %T", ErrInvalidMsgType, msg) 32 | } 33 | return nil 34 | } 35 | } 36 | 37 | func AFTType(typ string) func(m proto.Message) error { 38 | return func(msg proto.Message) error { 39 | if msg == nil { 40 | return ErrInvalidMsgType 41 | } 42 | if typ == "" { 43 | return nil 44 | } 45 | switch msg := msg.ProtoReflect().Interface().(type) { 46 | case *spb.GetRequest: 47 | switch strings.ToUpper(typ) { 48 | case "ALL": 49 | msg.Aft = spb.AFTType_ALL 50 | case "IPV4": 51 | msg.Aft = spb.AFTType_IPV4 52 | case "IPV6": 53 | msg.Aft = spb.AFTType_IPV6 54 | case "MPLS": 55 | msg.Aft = spb.AFTType_MPLS 56 | case "NEXTHOP", "NH": 57 | msg.Aft = spb.AFTType_NEXTHOP 58 | case "NEXTHOP_GROUP", "NEXTHOP-GROUP", "NHG": 59 | msg.Aft = spb.AFTType_NEXTHOP_GROUP 60 | case "MAC": 61 | msg.Aft = spb.AFTType_MAC 62 | case "POLICY_FORWARDING", "POLICY-FORWARDING", "PF": 63 | msg.Aft = spb.AFTType_POLICY_FORWARDING 64 | default: 65 | return fmt.Errorf("option AFTType: %w: %v", ErrInvalidValue, typ) 66 | } 67 | default: 68 | return fmt.Errorf("option AFTType: %w: %T", ErrInvalidMsgType, msg) 69 | } 70 | return nil 71 | } 72 | } 73 | 74 | func AFTTypeAll() func(m proto.Message) error { 75 | return AFTType("ALL") 76 | } 77 | 78 | func AFTTypeIPv4() func(m proto.Message) error { 79 | return AFTType("IPV4") 80 | } 81 | 82 | func AFTTypeIPv6() func(m proto.Message) error { 83 | return AFTType("IPV6") 84 | } 85 | 86 | func AFTTypeMPLS() func(m proto.Message) error { 87 | return AFTType("MPLS") 88 | } 89 | 90 | func AFTTypeNEXTHOP() func(m proto.Message) error { 91 | return AFTType("NEXTHOP") 92 | } 93 | 94 | func AFTTypNHG() func(m proto.Message) error { 95 | return AFTType("NHG") 96 | } 97 | 98 | func AFTTypeMAC() func(m proto.Message) error { 99 | return AFTType("MAC") 100 | } 101 | 102 | func AFTTypePF() func(m proto.Message) error { 103 | return AFTType("PF") 104 | } 105 | -------------------------------------------------------------------------------- /examples/operations/add_oper_single_primary.yaml: -------------------------------------------------------------------------------- 1 | # the network instance name to be used if none is 2 | # set under an operation configuration. 3 | default-network-instance: default 4 | 5 | params: 6 | redundancy: single-primary 7 | persistence: preserve 8 | ack-type: fib 9 | 10 | # list of operations to send towards targets, 11 | # only NH, NHG and IPv4 are supported. 12 | operations: 13 | - op: add 14 | nhg: 15 | id: 1 16 | # backup-nhg: # uint 17 | # color: # uint 18 | next-hop: 19 | - index: 1 20 | # weight: 1 # uint 21 | - index: 2 22 | # programmed-id: # uint 23 | 24 | - op: add 25 | # network-instance: # 26 | # election-id: # 27 | nh: 28 | index: 1 29 | ip-address: 192.168.1.2 30 | # interface-reference: 31 | # interface: 32 | # subinterface: 33 | # ip-in-ip: 34 | # dst-ip: 35 | # src-ip: 36 | # mac: 37 | # network-instance: 38 | # programmed-index: 39 | # pushed-mpls-label-stack: 40 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 41 | # label: # uint 42 | 43 | - op: add 44 | # network-instance: # 45 | # election-id: # 46 | nh: 47 | index: 2 48 | ip-address: 192.168.2.2 49 | # interface-reference: 50 | # interface: 51 | # subinterface: 52 | # ip-in-ip: 53 | # dst-ip: 54 | # src-ip: 55 | # mac: 56 | # network-instance: 57 | # programmed-index: 58 | # pushed-mpls-label-stack: 59 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 60 | # label: # uint 61 | 62 | - op: add 63 | # network-instance: not_default 64 | ipv4: 65 | prefix: 1.1.1.0/24 66 | nhg: 1 67 | nhg-network-instance: default 68 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 69 | # entry-metadata: # string 70 | 71 | - op: add 72 | network-instance: ns1 73 | # election-id: # 74 | nh: 75 | index: 1 76 | ip-address: 192.168.1.2 77 | # interface-reference: 78 | # interface: 79 | # subinterface: 80 | # ip-in-ip: 81 | # dst-ip: 82 | # src-ip: 83 | # mac: 84 | # network-instance: 85 | # programmed-index: 86 | # pushed-mpls-label-stack: 87 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 88 | # label: # uint 89 | 90 | - op: add 91 | network-instance: ns1 92 | nhg: 93 | id: 1 94 | # backup-nhg: # uint 95 | # color: # uint 96 | next-hop: 97 | - index: 1 98 | # weight: 1 # uint 99 | # programmed-id: # uint 100 | 101 | - op: add 102 | network-instance: ns1 103 | ipv4: 104 | prefix: 1.1.1.0/24 105 | nhg: 1 106 | nhg-network-instance: ns1 107 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 108 | # entry-metadata: # string -------------------------------------------------------------------------------- /examples/operations/add_oper_all_primary.yaml: -------------------------------------------------------------------------------- 1 | # the network instance name to be used if none is 2 | # set under an operation configuration. 3 | default-network-instance: default 4 | 5 | # params: 6 | # redundancy: # all-primary 7 | # persistence: # delete 8 | # ack-type: # rib-fib 9 | 10 | # list of operations to send towards targets, 11 | # only NH, NHG and IPv4 are supported. 12 | operations: 13 | - op: add 14 | nhg: 15 | id: 1 16 | # backup-nhg: # uint 17 | # color: # uint 18 | next-hop: 19 | - index: 1 20 | # weight: 1 # uint 21 | - index: 2 22 | # programmed-id: # uint 23 | 24 | - op: add 25 | # network-instance: # 26 | # election-id: # 27 | nh: 28 | index: 1 29 | ip-address: 192.168.1.2 30 | # interface-reference: 31 | # interface: 32 | # subinterface: 33 | # ip-in-ip: 34 | # dst-ip: 35 | # src-ip: 36 | # mac: 37 | # network-instance: 38 | # programmed-index: 39 | # pushed-mpls-label-stack: 40 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 41 | # label: # uint 42 | 43 | - op: add 44 | # network-instance: # 45 | # election-id: # 46 | nh: 47 | index: 2 48 | ip-address: 192.168.2.2 49 | # interface-reference: 50 | # interface: 51 | # subinterface: 52 | # ip-in-ip: 53 | # dst-ip: 54 | # src-ip: 55 | # mac: 56 | # network-instance: 57 | # programmed-index: 58 | # pushed-mpls-label-stack: 59 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 60 | # label: # uint 61 | 62 | - op: add 63 | # network-instance: not_default 64 | ipv4: 65 | prefix: 1.1.1.0/24 66 | nhg: 1 67 | nhg-network-instance: default 68 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 69 | # entry-metadata: # string 70 | 71 | - op: add 72 | network-instance: ns1 73 | # election-id: # 74 | nh: 75 | index: 1 76 | ip-address: 192.168.1.2 77 | # interface-reference: 78 | # interface: 79 | # subinterface: 80 | # ip-in-ip: 81 | # dst-ip: 82 | # src-ip: 83 | # mac: 84 | # network-instance: 85 | # programmed-index: 86 | # pushed-mpls-label-stack: 87 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 88 | # label: # uint 89 | 90 | - op: add 91 | network-instance: ns1 92 | nhg: 93 | id: 1 94 | # backup-nhg: # uint 95 | # color: # uint 96 | next-hop: 97 | - index: 1 98 | # weight: 1 # uint 99 | # programmed-id: # uint 100 | 101 | - op: add 102 | network-instance: ns1 103 | ipv4: 104 | prefix: 1.1.1.0/24 105 | nhg: 1 106 | nhg-network-instance: ns1 107 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 108 | # entry-metadata: # string -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: gRIBIc 2 | nav: 3 | - Home: index.md 4 | 5 | - Getting started: 6 | - Installation: install.md 7 | - User guide: user_guide.md 8 | - Command reference: cmd/get.md 9 | - Command reference: 10 | - Get: cmd/get.md 11 | - Flush: cmd/flush.md 12 | - Modify: cmd/modify.md 13 | 14 | site_author: Karim Radhouani 15 | site_description: >- 16 | Openconfig gRIBI client 17 | # Repository 18 | repo_name: karimra/gribic 19 | repo_url: https://github.com/karimra/gribic 20 | edit_uri: "" 21 | theme: 22 | name: material 23 | 24 | features: 25 | - navigation.tabs 26 | #- navigation.expand 27 | - navigation.top 28 | #- navigation.sections 29 | 30 | # 404 page 31 | static_templates: 32 | - 404.html 33 | 34 | # Don't include MkDocs' JavaScript 35 | include_search_page: false 36 | search_index_only: true 37 | 38 | # Default values, taken from mkdocs_theme.yml 39 | language: en 40 | palette: 41 | # Light mode 42 | - media: "(prefers-color-scheme: light)" 43 | scheme: default 44 | primary: blue 45 | accent: indigo 46 | toggle: 47 | icon: material/toggle-switch-off-outline 48 | name: Switch to dark mode 49 | # Dark mode 50 | - media: "(prefers-color-scheme: dark)" 51 | scheme: slate 52 | primary: black 53 | accent: cyan 54 | toggle: 55 | icon: material/toggle-switch 56 | name: Switch to light mode 57 | 58 | font: 59 | text: Manrope 60 | code: Fira Mono 61 | icon: 62 | logo: octicons/pulse-24 63 | favicon: images/pulse.svg 64 | 65 | extra_css: 66 | - stylesheets/extra.css 67 | 68 | # Plugins 69 | plugins: 70 | - search 71 | - minify: 72 | minify_html: true 73 | 74 | # Customization 75 | extra: 76 | social: 77 | - icon: fontawesome/brands/github 78 | link: https://github.com/karimra 79 | analytics: 80 | provider: google 81 | property: UA-177206500-1 82 | 83 | # Extensions 84 | markdown_extensions: 85 | - markdown.extensions.admonition 86 | - markdown.extensions.attr_list 87 | - markdown.extensions.codehilite: 88 | guess_lang: false 89 | - markdown.extensions.def_list 90 | - markdown.extensions.footnotes 91 | - markdown.extensions.meta 92 | - markdown.extensions.toc: 93 | permalink: "#" 94 | - pymdownx.arithmatex 95 | - pymdownx.betterem: 96 | smart_enable: all 97 | - pymdownx.caret 98 | - pymdownx.critic 99 | - pymdownx.details 100 | - pymdownx.emoji: 101 | emoji_index: !!python/name:materialx.emoji.twemoji 102 | emoji_generator: !!python/name:materialx.emoji.to_svg 103 | - pymdownx.highlight: 104 | linenums_style: pymdownx-inline 105 | - pymdownx.inlinehilite 106 | - pymdownx.keys 107 | - pymdownx.magiclink: 108 | repo_url_shorthand: true 109 | user: squidfunk 110 | repo: mkdocs-material 111 | - pymdownx.mark 112 | - pymdownx.smartsymbols 113 | - pymdownx.snippets: 114 | check_paths: true 115 | - pymdownx.superfences 116 | - pymdownx.tabbed 117 | - pymdownx.tasklist: 118 | custom_checkbox: true 119 | - pymdownx.tilde 120 | -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # Flags 2 | 3 | 4 | ## Global Flags 5 | 6 | ### config 7 | 8 | The `--config` flag specifies the location of a configuration file that `gribic` will read. 9 | 10 | If not specified, gribic searches for a file named `.gribic` with extensions `yaml, yml, toml or json` in the following locations: 11 | 12 | * `$PWD` 13 | * `$HOME` 14 | * `$XDG_CONFIG_HOME` 15 | * `$XDG_CONFIG_HOME/gribic` 16 | 17 | ### address 18 | 19 | The address flag `[-a | --address]` is used to specify the gRIBI server address in address:port format, for e.g: `192.168.113.11:57400` 20 | 21 | Multiple target addresses can be specified, either as comma separated values: 22 | 23 | ```bash 24 | gribic --address 192.168.113.11:57400,192.168.113.12:57400 25 | ``` 26 | 27 | or by using the `--address` flag multiple times: 28 | 29 | ```bash 30 | gribic -a 192.168.113.11:57400 --address 192.168.113.12:57400 31 | ``` 32 | 33 | The port number can be omitted, in which case the value fro m the flag --port will be appended to the address 34 | 35 | ### username 36 | 37 | The username flag `[-u | --username]` is used to specify the target username as part of the user credentials 38 | 39 | ### password 40 | 41 | The password flag `[-p | --password]` is used to specify the target password as part of the user credentials. 42 | 43 | ### port 44 | 45 | ### insecure 46 | 47 | The insecure flag `[--insecure]` is used to indicate that the client wishes to establish an non-TLS enabled gRPC connection. 48 | 49 | To disable certificate validation in a TLS-enabled connection use [`skip-verify`](#skip-verify) flag. 50 | 51 | ### skip-verify 52 | 53 | The skip verify flag `[--skip-verify]` indicates that the target should skip the signature verification steps, in case a secure connection is used. 54 | 55 | ### tls-ca 56 | 57 | The TLS CA flag `[--tls-ca]` specifies the root certificates for verifying server certificates encoded in PEM format. 58 | 59 | ### tls-cert 60 | 61 | The tls cert flag `[--tls-cert]` specifies the public key for the client encoded in PEM format. 62 | 63 | ### tls-key 64 | 65 | The tls key flag `[--tls-key]` specifies the private key for the client encoded in PEM format. 66 | 67 | ### timeout 68 | 69 | The timeout flag `[--timeout]` specifies the gRPC timeout after which the connection attempt fails. 70 | 71 | Valid formats: 10s, 1m30s, 1h. Defaults to 10s 72 | 73 | ### debug 74 | 75 | The debug flag `[-d | --debug]` enables the printing of extra information when sending/receiving an RPC 76 | 77 | ### proxy-from-env 78 | 79 | The proxy-from-env flag `[--proxy-from-env]` indicates that the gribic should use the HTTP/HTTPS proxy addresses defined in the environment variables `http_proxy` and `https_proxy` to reach the targets specified using the `--address` flag. 80 | 81 | 82 | 83 | ### election-id 84 | 85 | The Election ID flag `--election-id` is used to specify the election ID used with the Flush and Modify RPCs 86 | 87 | It takes a string in the format `high:low` where both high and low are uint64 forming a uint128 election ID value. 88 | 89 | `:`, `1:` and `:1` are valid values. 90 | 91 | ### max-rcv-msg-size 92 | 93 | The `--max-rcv-msg-size` set the maximum message size the client can receive from the server. defaults to 4MB 94 | 95 | ## Targets 96 | 97 | TODO 98 | -------------------------------------------------------------------------------- /examples/workflow/workflow1.yaml: -------------------------------------------------------------------------------- 1 | name: wf1 2 | 3 | steps: 4 | # steps can be named, 5 | # if not, the default name will be ${workflow-name}.${idx} where $idx is the step number 6 | - name: step1 7 | wait-after: 1s 8 | rpc: get 9 | network-instance: default # "" == all 10 | aft: # ipv4, ipv6, nh, nhg 11 | 12 | - # name: wf1.2 13 | rpc: flush 14 | # election-if for flush request 15 | # election-id: 1:1 16 | 17 | # overrides existing election-id on the router 18 | override: true 19 | 20 | # which network instance to flush 21 | network-instance: default # all if empty 22 | 23 | - rpc: modify 24 | # wait: duration, time to wait before running the step. 25 | # wait: 0s 26 | # 27 | # wait-after: duration, time to wait after running the step 28 | wait-after: 1s 29 | 30 | # election-id format uint64:uint64, high:low 31 | election-id: 1:2 32 | 33 | # modify request params 34 | session-params: 35 | redundancy: single-primary # all-primary 36 | persistence: preserve # delete 37 | ack-type: rib # rib-fib 38 | 39 | # modify request operations 40 | operations: 41 | - id: 1 42 | # election-id: 1:2 43 | op: add # delete, replace 44 | network-instance: default 45 | nh: # next hop 46 | index: 1 # nh index 47 | ip-address: 192.168.1.2 # nh ip address 48 | # interface-reference: 49 | # interface: 50 | # subinterface: 51 | # ip-in-ip: 52 | # dst-ip: 53 | # src-ip: 54 | # mac: 55 | # network-instance: 56 | # programmed-index: 57 | # pushed-mpls-label-stack: 58 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 59 | # label: # uint 60 | - id: 2 61 | op: add # delete, replace 62 | network-instance: default 63 | nhg: # next hop group 64 | id: 1 # nhg id 65 | next-hop: # next hops 66 | - index: 1 67 | 68 | - rpc: modify 69 | # election-id: 70 | wait: 1s 71 | # wait-after: 1s 72 | operations: 73 | - id: 3 74 | election-id: 75 | op: add # delete, replace 76 | network-instance: default 77 | nh: # next hop, nhg, ipv4, ipv6 78 | index: 2 # nh index 79 | ip-address: 192.168.1.3 # nh ip address 80 | # interface-reference: 81 | # interface: 82 | # subinterface: 83 | # ip-in-ip: 84 | # dst-ip: 85 | # src-ip: 86 | # mac: 87 | # network-instance: 88 | # programmed-index: 89 | # pushed-mpls-label-stack: 90 | # - type: # ipv4-explicit, router-alert, ipv6-explicit, implicit, entropy-label-indicator, no-label 91 | # label: # uint 92 | - id: 4 93 | op: add 94 | network-instance: default 95 | ipv4: 96 | prefix: 1.1.1.0/24 97 | nhg: 1 98 | # nhg-network-instance: ns1 99 | # decapsulate-header: # enum: gre, ipv4, ipv6, mpls 100 | # entry-metadata: # string 101 | 102 | - rpc: get 103 | wait: 1s 104 | # wait-after: 1s 105 | network-instance: default 106 | aft: all # 107 | -------------------------------------------------------------------------------- /docs/cmd/modify.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | The Modify Command runs a [gRIBI Modify RPC](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L31) as a client, sending a stream of [ModifyRequest(s)](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L52) to a gRIBI server. 4 | The Server returns a stream of [ModifyResponse(s)](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L213). 5 | 6 | The ModifyRequest is used to set the current session parameters (redundancy, persistence, and ack mode) as well as issuing AFT operation to the server. 7 | 8 | The AFT operation can be an ADD, REPLACE or DELETE and references a single AFT entry of type IPV4, IPv6, Next Hop, Next Hop Group, MPLS, MAC or Policy Forwarding. 9 | 10 | A single modifyRequest can carry multiple AFT operations. 11 | 12 | A Modify RPC start with the client sending a ModifyRequest indicating the [session parameters](https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto#L342) it wants to apply to the current session, the parameters are: 13 | 14 | - Redundancy: specifies the client redundancy mode, either `ALL_PRIMARY` or `SINGLE_PRIMARY` 15 | - `ALL_PRIMARY`: is the default and indicates that the server should accept AFT operations from all clients. 16 | 17 | When it comes to ADD operations, the server should add an entry when it first receives it from any client. 18 | While it should wait for the last delete to remove it from its RIB. 19 | 20 | In other words, the server should keep track of the number of clients it received a specific entry from. 21 | 22 | - `SINGLE_PRIMARY`: implies that the clients went through an election process and a single one came out as primary, it has the highest election ID which it sends to the server in the initial ModifyRequest as well as with each AFT Operation. 23 | 24 | The server accepts AFT Operations only from the client with the highest election ID. 25 | 26 | - Persistence: Specifies desired server behavior when the client disconnects. 27 | - `DELETE`: is the default, it means that all AFTs received from the client shall be deleted when it disconnects. 28 | - `PRESERVE`: the server should keep the RIB and FIB entries set by the client when it disconnects. 29 | 30 | - Ack Mode: Specifies the Ack type expected by the client 31 | - `RIB_ACK`: the server must respond with `RIB_PROGRAMMED` 32 | - `RIB_AND_FIB_ACK`: the server must respond with `RIB_PROGRAMMED`, if the AFT entry is also programmed in the NE FIB, the server must response with `FIB_PROGRAMMED` instead. 33 | 34 | ### Usage 35 | 36 | `gribic [global-flags] modify [local-flags]` 37 | 38 | Alias: `mod`, `m` 39 | 40 | ### Flags 41 | 42 | #### single-primary 43 | 44 | The `--single-primary` flag set the session parameters redundancy to `SINGLE_PRIMARY` 45 | 46 | #### preserve 47 | 48 | The `--preserve` flag set the session parameters persistence to `PRESERVE` 49 | 50 | #### fib 51 | 52 | The `--fib` flag set the session parameters Ack mode to `RIB_AND_FIB_ACK` 53 | 54 | #### input-file 55 | 56 | The `--input-file` flag points to a modify input file 57 | 58 | See [here](https://github.com/karimra/gribic/examples) for some input file examples 59 | 60 | ### Examples 61 | 62 | Run all operations defined in the input-file in `single-primary` redundancy mode, with persistence `preserve` and ack mode `RIB_FIB` 63 | 64 | ```bash 65 | gribic -a router1 -u admin -p admin --skip-verify modify \ 66 | --single-primary \ 67 | --preserve \ 68 | --fib \ 69 | --election-id 1:2 \ 70 | --input-file 71 | ``` 72 | -------------------------------------------------------------------------------- /api/modify.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | spb "github.com/openconfig/gribi/v1/proto/service" 5 | "google.golang.org/protobuf/proto" 6 | ) 7 | 8 | func NewModifyRequest(opts ...GRIBIOption) (*spb.ModifyRequest, error) { 9 | m := new(spb.ModifyRequest) 10 | err := apply(m, opts...) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return m, nil 15 | } 16 | 17 | func RedundancyAllPrimary() func(proto.Message) error { 18 | return func(msg proto.Message) error { 19 | if msg == nil { 20 | return ErrInvalidMsgType 21 | } 22 | switch msg := msg.ProtoReflect().Interface().(type) { 23 | case *spb.ModifyRequest: 24 | if msg.Params == nil { 25 | msg.Params = new(spb.SessionParameters) 26 | } 27 | msg.Params.Redundancy = spb.SessionParameters_ALL_PRIMARY 28 | } 29 | return nil 30 | } 31 | } 32 | 33 | func RedundancySinglePrimary() func(proto.Message) error { 34 | return func(msg proto.Message) error { 35 | if msg == nil { 36 | return ErrInvalidMsgType 37 | } 38 | switch msg := msg.ProtoReflect().Interface().(type) { 39 | case *spb.ModifyRequest: 40 | if msg.Params == nil { 41 | msg.Params = new(spb.SessionParameters) 42 | } 43 | msg.Params.Redundancy = spb.SessionParameters_SINGLE_PRIMARY 44 | } 45 | return nil 46 | } 47 | } 48 | 49 | func PersistenceDelete() func(proto.Message) error { 50 | return func(msg proto.Message) error { 51 | if msg == nil { 52 | return ErrInvalidMsgType 53 | } 54 | switch msg := msg.ProtoReflect().Interface().(type) { 55 | case *spb.ModifyRequest: 56 | if msg.Params == nil { 57 | msg.Params = new(spb.SessionParameters) 58 | } 59 | msg.Params.Persistence = spb.SessionParameters_DELETE 60 | } 61 | return nil 62 | } 63 | } 64 | 65 | func PersistencePreserve() func(proto.Message) error { 66 | return func(msg proto.Message) error { 67 | if msg == nil { 68 | return ErrInvalidMsgType 69 | } 70 | switch msg := msg.ProtoReflect().Interface().(type) { 71 | case *spb.ModifyRequest: 72 | if msg.Params == nil { 73 | msg.Params = new(spb.SessionParameters) 74 | } 75 | msg.Params.Persistence = spb.SessionParameters_PRESERVE 76 | } 77 | return nil 78 | } 79 | } 80 | 81 | func AckTypeRib() func(proto.Message) error { 82 | return func(msg proto.Message) error { 83 | if msg == nil { 84 | return ErrInvalidMsgType 85 | } 86 | switch msg := msg.ProtoReflect().Interface().(type) { 87 | case *spb.ModifyRequest: 88 | if msg.Params == nil { 89 | msg.Params = new(spb.SessionParameters) 90 | } 91 | msg.Params.AckType = spb.SessionParameters_RIB_ACK 92 | } 93 | return nil 94 | } 95 | } 96 | 97 | func AckTypeRibFib() func(proto.Message) error { 98 | return func(msg proto.Message) error { 99 | if msg == nil { 100 | return ErrInvalidMsgType 101 | } 102 | switch msg := msg.ProtoReflect().Interface().(type) { 103 | case *spb.ModifyRequest: 104 | if msg.Params == nil { 105 | msg.Params = new(spb.SessionParameters) 106 | } 107 | msg.Params.AckType = spb.SessionParameters_RIB_AND_FIB_ACK 108 | } 109 | return nil 110 | } 111 | } 112 | 113 | func AFTOperation(opts ...GRIBIOption) func(proto.Message) error { 114 | return func(msg proto.Message) error { 115 | if msg == nil { 116 | return ErrInvalidMsgType 117 | } 118 | switch msg := msg.ProtoReflect().Interface().(type) { 119 | case *spb.ModifyRequest: 120 | aftOper, err := NewAFTOperation(opts...) 121 | if err != nil { 122 | return err 123 | } 124 | if len(msg.Operation) == 0 { 125 | msg.Operation = make([]*spb.AFTOperation, 0) 126 | } 127 | msg.Operation = append(msg.Operation, aftOper) 128 | } 129 | return nil 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/flush.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/karimra/gribic/api" 9 | "github.com/karimra/gribic/config" 10 | spb "github.com/openconfig/gribi/v1/proto/service" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/pflag" 13 | "google.golang.org/protobuf/encoding/prototext" 14 | ) 15 | 16 | type flushResponse struct { 17 | TargetError 18 | // req *spb.FlushResponse 19 | rsp *spb.FlushResponse 20 | } 21 | 22 | func (a *App) InitFlushFlags(cmd *cobra.Command) { 23 | cmd.ResetFlags() 24 | // 25 | cmd.Flags().StringVarP(&a.Config.FlushNetworkInstance, "ns", "", "", "network instance name") 26 | cmd.Flags().BoolVarP(&a.Config.FlushNetworkInstanceAll, "ns-all", "", false, "run Get against all network instance(s)") 27 | 28 | cmd.Flags().BoolVarP(&a.Config.FlushElectionIDOverride, "override", "", false, "override election ID") 29 | // 30 | cmd.Flags().VisitAll(func(flag *pflag.Flag) { 31 | a.Config.FileConfig.BindPFlag(fmt.Sprintf("%s-%s", cmd.Name(), flag.Name), flag) 32 | }) 33 | } 34 | 35 | func (a *App) FlushPreRunE(cmd *cobra.Command, args []string) error { 36 | // parse election ID 37 | if flagIsSet(cmd, "election-id") { 38 | var err error 39 | a.electionID, err = config.ParseUint128(a.Config.GlobalFlags.ElectionID) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | if !a.Config.FlushNetworkInstanceAll && a.Config.FlushNetworkInstance == "" { 45 | return errors.New("set a specific network-instance name to flush using --ns or flush all network-instances with --ns-all") 46 | } 47 | return nil 48 | } 49 | 50 | func (a *App) FlushRunE(cmd *cobra.Command, args []string) error { 51 | targets, err := a.GetTargets() 52 | if err != nil { 53 | return err 54 | } 55 | a.Logger.Debugf("targets: %v", targets) 56 | numTargets := len(targets) 57 | responseChan := make(chan *flushResponse, numTargets) 58 | 59 | a.wg.Add(numTargets) 60 | for _, t := range targets { 61 | go func(t *target) { 62 | defer a.wg.Done() 63 | // create context 64 | ctx, cancel := context.WithCancel(a.ctx) 65 | defer cancel() 66 | // append credentials to context 67 | ctx = appendCredentials(ctx, t.Config) 68 | // create a grpc conn 69 | err = a.CreateGrpcClient(ctx, t, a.createBaseDialOpts()...) 70 | if err != nil { 71 | responseChan <- &flushResponse{ 72 | TargetError: TargetError{ 73 | TargetName: t.Config.Address, 74 | Err: err, 75 | }, 76 | } 77 | return 78 | } 79 | defer t.Close() 80 | rsp, err := a.gribiFlush(ctx, t) 81 | responseChan <- &flushResponse{ 82 | TargetError: TargetError{ 83 | TargetName: t.Config.Address, 84 | Err: err, 85 | }, 86 | rsp: rsp, 87 | } 88 | }(t) 89 | } 90 | // 91 | a.wg.Wait() 92 | close(responseChan) 93 | 94 | errs := make([]error, 0) //, numTargets) 95 | result := make([]*flushResponse, 0, numTargets) 96 | for rsp := range responseChan { 97 | if rsp.Err != nil { 98 | wErr := fmt.Errorf("%q Flush RPC failed: %v", rsp.TargetName, rsp.Err) 99 | a.Logger.Error(wErr) 100 | errs = append(errs, wErr) 101 | continue 102 | } 103 | result = append(result, rsp) 104 | } 105 | a.Logger.Printf("got %d results", len(result)) 106 | for _, r := range result { 107 | a.Logger.Infof("%q: %s", r.TargetName, prototext.Format(r.rsp)) 108 | } 109 | return a.handleErrs(errs) 110 | } 111 | 112 | func (a *App) gribiFlush(ctx context.Context, t *target) (*spb.FlushResponse, error) { 113 | opts := make([]api.GRIBIOption, 0, 2) 114 | switch { 115 | case a.Config.FlushNetworkInstanceAll: 116 | opts = append(opts, api.NSAll()) 117 | default: 118 | opts = append(opts, api.NetworkInstance(a.Config.FlushNetworkInstance)) 119 | } 120 | switch { 121 | case a.Config.FlushElectionIDOverride: 122 | opts = append(opts, api.Override()) 123 | case a.electionID != nil: 124 | opts = append(opts, api.ElectionID(a.electionID)) 125 | } 126 | req, err := api.NewFlushRequest(opts...) 127 | if err != nil { 128 | return nil, err 129 | } 130 | a.Logger.Debugf("target %s request:\n%s", t.Config.Name, prototext.Format(req)) 131 | t.gRIBIClient = spb.NewGRIBIClient(t.conn) 132 | return a.flush(ctx, t, req) 133 | } 134 | 135 | func (a *App) flush(ctx context.Context, t *target, req *spb.FlushRequest) (*spb.FlushResponse, error) { 136 | return t.gRIBIClient.Flush(ctx, req) 137 | } 138 | -------------------------------------------------------------------------------- /app/get.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/karimra/gribic/api" 9 | spb "github.com/openconfig/gribi/v1/proto/service" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/pflag" 12 | "google.golang.org/protobuf/encoding/prototext" 13 | ) 14 | 15 | type getResponse struct { 16 | TargetError 17 | rsp []*spb.GetResponse 18 | } 19 | 20 | func (a *App) InitGetFlags(cmd *cobra.Command) { 21 | cmd.ResetFlags() 22 | // 23 | cmd.Flags().StringVarP(&a.Config.GetNetworkInstance, "ns", "", "", "network instance name, an empty network-instance name means query all instances.") 24 | cmd.Flags().StringVarP(&a.Config.GetAFT, "aft", "", "ALL", "AFT type, one of: ALL, IPv4, IPv6, NH, NHG, MPLS, MAC or PF") 25 | 26 | // 27 | cmd.Flags().VisitAll(func(flag *pflag.Flag) { 28 | a.Config.FileConfig.BindPFlag(fmt.Sprintf("%s-%s", cmd.Name(), flag.Name), flag) 29 | }) 30 | } 31 | 32 | func (a *App) GetRunE(cmd *cobra.Command, args []string) error { 33 | targets, err := a.GetTargets() 34 | if err != nil { 35 | return err 36 | } 37 | a.Logger.Debugf("targets: %v", targets) 38 | numTargets := len(targets) 39 | responseChan := make(chan *getResponse, numTargets) 40 | 41 | a.wg.Add(numTargets) 42 | for _, t := range targets { 43 | go func(t *target) { 44 | defer a.wg.Done() 45 | // create context 46 | ctx, cancel := context.WithCancel(a.ctx) 47 | defer cancel() 48 | // append credentials to context 49 | ctx = appendCredentials(ctx, t.Config) 50 | // create a grpc conn 51 | err = a.CreateGrpcClient(ctx, t, a.createBaseDialOpts()...) 52 | if err != nil { 53 | responseChan <- &getResponse{ 54 | TargetError: TargetError{ 55 | TargetName: t.Config.Address, 56 | Err: err, 57 | }, 58 | } 59 | return 60 | } 61 | defer t.Close() 62 | rsp, err := a.gribiGet(ctx, t) 63 | responseChan <- &getResponse{ 64 | TargetError: TargetError{ 65 | TargetName: t.Config.Address, 66 | Err: err, 67 | }, 68 | rsp: []*spb.GetResponse{rsp}, 69 | } 70 | }(t) 71 | } 72 | // 73 | a.wg.Wait() 74 | close(responseChan) 75 | 76 | errs := make([]error, 0) //, numTargets) 77 | result := make([]*getResponse, 0, numTargets) 78 | for rsp := range responseChan { 79 | if rsp.Err != nil { 80 | wErr := fmt.Errorf("%q Get RPC failed: %v", rsp.TargetName, rsp.Err) 81 | a.Logger.Error(wErr) 82 | errs = append(errs, wErr) 83 | continue 84 | } 85 | result = append(result, rsp) 86 | } 87 | a.Logger.Printf("got %d results", len(result)) 88 | for _, r := range result { 89 | for _, gr := range r.rsp { 90 | a.Logger.Infof("%q:\n%v", r.TargetName, prototext.Format(gr)) 91 | } 92 | } 93 | return a.handleErrs(errs) 94 | } 95 | 96 | func (a *App) gribiGet(ctx context.Context, t *target) (*spb.GetResponse, error) { 97 | opts := make([]api.GRIBIOption, 0, 2) 98 | opts = append(opts, api.AFTType(a.Config.GetAFT)) 99 | if a.Config.GetNetworkInstance == "" { 100 | opts = append(opts, api.NSAll()) 101 | } else { 102 | opts = append(opts, api.NetworkInstance(a.Config.GetNetworkInstance)) 103 | } 104 | 105 | req, err := api.NewGetRequest(opts...) 106 | if err != nil { 107 | return nil, err 108 | } 109 | t.gRIBIClient = spb.NewGRIBIClient(t.conn) 110 | return a.get(ctx, t, req) 111 | } 112 | 113 | func (a *App) get(ctx context.Context, t *target, req *spb.GetRequest) (*spb.GetResponse, error) { 114 | stream, err := t.gRIBIClient.Get(ctx, req) 115 | if err != nil { 116 | return nil, err 117 | } 118 | resp := &spb.GetResponse{ 119 | Entry: make([]*spb.AFTEntry, 0), 120 | } 121 | for { 122 | getres, err := stream.Recv() 123 | if err == io.EOF { 124 | a.Logger.Debugf("target %s: received EOF", t.Config.Name) 125 | break 126 | } 127 | if err != nil { 128 | return nil, err 129 | } 130 | a.Logger.Debugf("target %s: intermediate get response: %v", t.Config.Name, getres) 131 | resp.Entry = append(resp.Entry, getres.GetEntry()...) 132 | } 133 | a.Logger.Infof("target %s: final get response: %+v", t.Config.Name, resp) 134 | return resp, nil 135 | } 136 | 137 | func (a *App) getChan(ctx context.Context, t *target, req *spb.GetRequest) (chan *spb.GetResponse, chan error) { 138 | rspChan := make(chan *spb.GetResponse) 139 | errChan := make(chan error) 140 | go func() { 141 | defer close(rspChan) 142 | defer close(errChan) 143 | stream, err := t.gRIBIClient.Get(ctx, req) 144 | if err != nil { 145 | errChan <- err 146 | return 147 | } 148 | for { 149 | getres, err := stream.Recv() 150 | if err != nil { 151 | errChan <- err 152 | return 153 | } 154 | rspChan <- getres 155 | } 156 | }() 157 | return rspChan, errChan 158 | } 159 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/karimra/gribic/config" 9 | "github.com/openconfig/gnmi/proto/gnmi" 10 | spb "github.com/openconfig/gribi/v1/proto/service" 11 | "github.com/prometheus/client_golang/prometheus" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | "golang.org/x/sync/semaphore" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/encoding/gzip" 17 | "google.golang.org/grpc/grpclog" 18 | "google.golang.org/grpc/metadata" 19 | ) 20 | 21 | type App struct { 22 | // 23 | mrcv *sync.Mutex 24 | // 25 | ctx context.Context 26 | Cfn context.CancelFunc 27 | RootCmd *cobra.Command 28 | 29 | pm *sync.Mutex 30 | // wg for cmd execution 31 | wg *sync.WaitGroup 32 | // gRIBIc config 33 | Config *config.Config 34 | // gRIBI client electionID 35 | electionID *spb.Uint128 36 | // gRIBI targets, ie routers 37 | m *sync.RWMutex 38 | Targets map[string]*target 39 | // gNMI server 40 | gnmi.UnimplementedGNMIServer 41 | grpcServer *grpc.Server 42 | unaryRPCsem *semaphore.Weighted 43 | // 44 | Logger *log.Entry 45 | // 46 | // prometheus registry 47 | reg *prometheus.Registry 48 | } 49 | 50 | func New() *App { 51 | ctx, cancel := context.WithCancel(context.Background()) 52 | logger := log.New() 53 | a := &App{ 54 | mrcv: new(sync.Mutex), 55 | ctx: ctx, 56 | Cfn: cancel, 57 | RootCmd: new(cobra.Command), 58 | pm: new(sync.Mutex), 59 | 60 | Config: config.New(), 61 | // 62 | m: new(sync.RWMutex), 63 | Targets: make(map[string]*target), 64 | wg: new(sync.WaitGroup), 65 | Logger: log.NewEntry(logger), 66 | } 67 | return a 68 | } 69 | 70 | func (a *App) Context() context.Context { 71 | if a.ctx == nil { 72 | return context.Background() 73 | } 74 | return a.ctx 75 | } 76 | 77 | func (a *App) InitGlobalFlags() { 78 | a.RootCmd.ResetFlags() 79 | 80 | a.RootCmd.PersistentFlags().StringVar(&a.Config.CfgFile, "config", "", "config file (default is $HOME/gribic.yaml)") 81 | a.RootCmd.PersistentFlags().StringSliceVarP(&a.Config.GlobalFlags.Address, "address", "a", []string{}, "comma separated gRIBI targets addresses") 82 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.Username, "username", "u", "", "username") 83 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.Password, "password", "p", "", "password") 84 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.Port, "port", "", defaultGrpcPort, "gRPC port") 85 | a.RootCmd.PersistentFlags().BoolVarP(&a.Config.GlobalFlags.Insecure, "insecure", "", false, "insecure connection") 86 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.TLSCa, "tls-ca", "", "", "tls certificate authority") 87 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.TLSCert, "tls-cert", "", "", "tls certificate") 88 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.TLSKey, "tls-key", "", "", "tls key") 89 | a.RootCmd.PersistentFlags().DurationVarP(&a.Config.GlobalFlags.Timeout, "timeout", "", 10*time.Second, "grpc timeout, valid formats: 10s, 1m30s, 1h") 90 | a.RootCmd.PersistentFlags().BoolVarP(&a.Config.GlobalFlags.Debug, "debug", "d", false, "debug mode") 91 | a.RootCmd.PersistentFlags().BoolVarP(&a.Config.GlobalFlags.SkipVerify, "skip-verify", "", false, "skip verify tls connection") 92 | a.RootCmd.PersistentFlags().BoolVarP(&a.Config.GlobalFlags.ProxyFromEnv, "proxy-from-env", "", false, "use proxy from environment") 93 | a.RootCmd.PersistentFlags().IntVarP(&a.Config.GlobalFlags.MaxRcvMsgSize, "max-rcv-msg-size", "", 1024*1024*4, "max receive message size in bytes") 94 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.Format, "format", "", "text", "output format, one of: text, json") 95 | // 96 | a.RootCmd.PersistentFlags().StringVarP(&a.Config.GlobalFlags.ElectionID, "election-id", "", "1:0", "gRIBI client electionID, format is high:low where both high and low are uint64") 97 | } 98 | 99 | func (a *App) PreRun(cmd *cobra.Command, args []string) error { 100 | // init logger 101 | a.Config.SetLogger() 102 | if a.Config.Debug { 103 | a.Logger.Logger.SetLevel(log.DebugLevel) 104 | grpclog.SetLogger(a.Logger) //lint:ignore SA1019 . 105 | } 106 | // a.Config.SetPersistantFlagsFromFile(a.RootCmd) 107 | return nil 108 | } 109 | 110 | func (a *App) CreateGrpcClient(ctx context.Context, t *target, opts ...grpc.DialOption) error { 111 | tOpts := make([]grpc.DialOption, 0, len(opts)+1) 112 | tOpts = append(tOpts, opts...) 113 | 114 | nOpts, err := t.Config.DialOpts() 115 | if err != nil { 116 | return err 117 | } 118 | tOpts = append(tOpts, nOpts...) 119 | timeoutCtx, cancel := context.WithTimeout(ctx, t.Config.Timeout) 120 | defer cancel() 121 | t.conn, err = grpc.DialContext(timeoutCtx, t.Config.Address, tOpts...) 122 | return err 123 | } 124 | 125 | func (a *App) createBaseDialOpts() []grpc.DialOption { 126 | opts := []grpc.DialOption{grpc.WithBlock()} 127 | if !a.Config.ProxyFromEnv { 128 | opts = append(opts, grpc.WithNoProxy()) 129 | } 130 | if a.Config.Gzip { 131 | opts = append(opts, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name))) 132 | } 133 | if a.Config.MaxRcvMsgSize != 0 { 134 | opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(a.Config.MaxRcvMsgSize))) 135 | } 136 | return opts 137 | } 138 | 139 | func appendCredentials(ctx context.Context, tc *config.TargetConfig) context.Context { 140 | if tc.Username != nil { 141 | ctx = metadata.AppendToOutgoingContext(ctx, "username", *tc.Username) 142 | } 143 | if tc.Password != nil { 144 | ctx = metadata.AppendToOutgoingContext(ctx, "password", *tc.Password) 145 | } 146 | return ctx 147 | } 148 | -------------------------------------------------------------------------------- /config/workflow.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/karimra/gnmic/utils" 13 | "github.com/karimra/gribic/api" 14 | spb "github.com/openconfig/gribi/v1/proto/service" 15 | "google.golang.org/protobuf/proto" 16 | "gopkg.in/yaml.v2" 17 | ) 18 | 19 | type Workflow struct { 20 | Name string `yaml:"name,omitempty"` 21 | Steps []*step `yaml:"steps,omitempty"` 22 | } 23 | 24 | type step struct { 25 | Name string `yaml:"name,omitempty"` 26 | Wait time.Duration `yaml:"wait,omitempty"` 27 | WaitAfter time.Duration `yaml:"wait-after,omitempty"` 28 | // determines the RPC type 29 | RPC string `yaml:"rpc,omitempty"` 30 | // network instance, applies if RPC is "get" or "flush" 31 | NetworkInstance string `yaml:"network-instance,omitempty"` 32 | // AFT type, applies if RPC is "get" or "flush" 33 | Aft string `yaml:"aft,omitempty"` 34 | // Override, applies if RPC is "flush" 35 | Override bool `yaml:"override,omitempty"` 36 | // Session Parameters for "modify RPC" 37 | SessionParams *sessionParams `yaml:"session-params,omitempty"` 38 | // ElectionID for "modify" with session parameters and "flush" RPCs 39 | ElectionID string `yaml:"election-id,omitempty"` 40 | // Operations for "modify" RPC 41 | Operations []*OperationConfig `yaml:"operations,omitempty"` 42 | } 43 | 44 | func (s *step) BuildRequests() ([]proto.Message, error) { 45 | switch strings.ToLower(s.RPC) { 46 | case "get": 47 | return s.buildGetRequest() 48 | case "flush": 49 | return s.buildFlushRequest() 50 | case "modify": 51 | return s.buildModifyRequest() 52 | } 53 | return nil, nil 54 | } 55 | 56 | func (s *step) buildGetRequest() ([]proto.Message, error) { 57 | opts := make([]api.GRIBIOption, 0) 58 | if s.NetworkInstance == "" { 59 | opts = append(opts, api.NSAll()) 60 | } else { 61 | opts = append(opts, api.NetworkInstance(s.NetworkInstance)) 62 | } 63 | if s.Aft == "" { 64 | opts = append(opts, api.AFTTypeAll()) 65 | } else { 66 | opts = append(opts, api.AFTType(s.Aft)) 67 | } 68 | 69 | req, err := api.NewGetRequest(opts...) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return []proto.Message{req}, nil 74 | } 75 | 76 | func (s *step) buildFlushRequest() ([]proto.Message, error) { 77 | opts := make([]api.GRIBIOption, 0, 2) 78 | if s.NetworkInstance == "" { 79 | opts = append(opts, api.NSAll()) 80 | } else { 81 | opts = append(opts, api.NetworkInstance(s.NetworkInstance)) 82 | } 83 | if s.Override { 84 | opts = append(opts, api.Override()) 85 | } else { 86 | eID, err := ParseUint128(s.ElectionID) 87 | if err != nil { 88 | return nil, err 89 | } 90 | opts = append(opts, api.ElectionID(eID)) 91 | } 92 | req, err := api.NewFlushRequest(opts...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return []proto.Message{req}, nil 97 | } 98 | 99 | func (s *step) buildModifyRequest() ([]proto.Message, error) { 100 | reqs := make([]proto.Message, 0, 2) 101 | opts := make([]api.GRIBIOption, 0, 4) 102 | if s.SessionParams != nil { 103 | // persistence 104 | if strings.ToLower(s.SessionParams.Persistence) == "preserve" { 105 | opts = append(opts, api.PersistencePreserve()) 106 | } 107 | // redundancy 108 | if strings.ToLower(s.SessionParams.Redundancy) == "single-primary" { 109 | opts = append(opts, 110 | api.RedundancySinglePrimary(), 111 | ) 112 | } 113 | // ack 114 | if s.SessionParams.AckType == "rib-fib" { 115 | opts = append(opts, api.AckTypeRibFib()) 116 | } 117 | 118 | req, err := api.NewModifyRequest(opts...) 119 | if err != nil { 120 | return nil, err 121 | } 122 | reqs = append(reqs, req) 123 | } 124 | // electionID if any 125 | var eID *spb.Uint128 126 | if s.ElectionID != "" { 127 | var err error 128 | eID, err = ParseUint128(s.ElectionID) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | if len(s.Operations) == 0 { 134 | req := &spb.ModifyRequest{ 135 | ElectionId: eID, 136 | } 137 | reqs = append(reqs, req) 138 | return reqs, nil 139 | } 140 | // aft modify if any 141 | req := &spb.ModifyRequest{ 142 | Operation: make([]*spb.AFTOperation, 0, len(s.Operations)), 143 | ElectionId: eID, 144 | } 145 | 146 | for _, op := range s.Operations { 147 | spbOp, err := op.CreateAftOper() 148 | if err != nil { 149 | return nil, err 150 | } 151 | req.Operation = append(req.Operation, spbOp) 152 | } 153 | reqs = append(reqs, req) 154 | return reqs, nil 155 | } 156 | 157 | func (c *Config) ReadWorkflowFile() error { 158 | c.logger.Infof("reading workflow file: %s", c.WorkflowFile) 159 | b, err := os.ReadFile(c.WorkflowFile) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | c.workflowTemplate, err = utils.CreateTemplate("workflow-template", string(b)) 165 | if err != nil { 166 | return err 167 | } 168 | return c.readWorkflowTemplateVarsFile() 169 | } 170 | 171 | func (c *Config) readWorkflowTemplateVarsFile() error { 172 | if c.WorkflowInputVarsFile == "" { 173 | ext := filepath.Ext(c.WorkflowFile) 174 | c.WorkflowInputVarsFile = fmt.Sprintf("%s%s%s", c.WorkflowFile[0:len(c.WorkflowFile)-len(ext)], varFileSuffix, ext) 175 | c.logger.Debugf("trying to find variable file %q", c.WorkflowInputVarsFile) 176 | _, err := os.Stat(c.WorkflowInputVarsFile) 177 | if os.IsNotExist(err) { 178 | c.WorkflowInputVarsFile = "" 179 | return nil 180 | } else if err != nil { 181 | return err 182 | } 183 | } 184 | b, err := readFile(c.WorkflowInputVarsFile) 185 | if err != nil { 186 | return err 187 | } 188 | if c.workflowVars == nil { 189 | c.workflowVars = make(map[string]interface{}) 190 | } 191 | err = yaml.Unmarshal(b, &c.workflowVars) 192 | if err != nil { 193 | return err 194 | } 195 | tempInterface := utils.Convert(c.workflowVars) 196 | switch t := tempInterface.(type) { 197 | case map[string]interface{}: 198 | c.workflowVars = t 199 | default: 200 | return errors.New("unexpected variables file format") 201 | } 202 | if c.Debug { 203 | c.logger.Printf("request vars content: %v", c.workflowVars) 204 | } 205 | fmt.Printf("workflow vars: %v\n", c.workflowVars) 206 | return nil 207 | } 208 | 209 | func (c *Config) GenerateWorkflow(targetName string) (*Workflow, error) { 210 | buf := new(bytes.Buffer) 211 | err := c.workflowTemplate.Execute(buf, 212 | templateInput{ 213 | TargetName: targetName, 214 | Vars: c.workflowVars, 215 | }, 216 | ) 217 | if err != nil { 218 | return nil, err 219 | } 220 | wf := new(Workflow) 221 | err = yaml.Unmarshal(buf.Bytes(), wf) 222 | // fmt.Printf("workflow for target=%q: %+v\n", targetName, wf) 223 | return wf, err 224 | } 225 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/adrg/xdg" 12 | "github.com/karimra/gnmic/utils" 13 | "github.com/mitchellh/go-homedir" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/pflag" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | const ( 21 | configName = ".gribic" 22 | envPrefix = "GRIBIC" 23 | ) 24 | 25 | type Config struct { 26 | GlobalFlags `mapstructure:",squash"` 27 | LocalFlags `mapstructure:",squash"` 28 | FileConfig *viper.Viper `mapstructure:"-" json:"-" yaml:"-" ` 29 | 30 | GnmiServer *gnmiServer `mapstructure:"gnmi-server,omitempty" json:"gnmi-server,omitempty" yaml:"gnmi-server,omitempty"` 31 | logger *log.Entry 32 | // 33 | modifyInputTemplate *template.Template 34 | modifyInputVars map[string]interface{} 35 | // 36 | workflowTemplate *template.Template 37 | workflowVars map[string]interface{} 38 | } 39 | 40 | type GlobalFlags struct { 41 | CfgFile string 42 | Address []string `mapstructure:"address,omitempty" json:"address,omitempty" yaml:"address,omitempty"` 43 | Username string `mapstructure:"username,omitempty" json:"username,omitempty" yaml:"username,omitempty"` 44 | Password string `mapstructure:"password,omitempty" json:"password,omitempty" yaml:"password,omitempty"` 45 | Port string `mapstructure:"port,omitempty" json:"port,omitempty" yaml:"port,omitempty"` 46 | Insecure bool `mapstructure:"insecure,omitempty" json:"insecure,omitempty" yaml:"insecure,omitempty"` 47 | TLSCa string `mapstructure:"tls-ca,omitempty" json:"tls-ca,omitempty" yaml:"tls-ca,omitempty"` 48 | TLSCert string `mapstructure:"tls-cert,omitempty" json:"tls-cert,omitempty" yaml:"tls-cert,omitempty"` 49 | TLSKey string `mapstructure:"tls-key,omitempty" json:"tls-key,omitempty" yaml:"tls-key,omitempty"` 50 | TLSMinVersion string `mapstructure:"tls-min-version,omitempty" json:"tls-min-version,omitempty" yaml:"tls-min-version,omitempty"` 51 | TLSMaxVersion string `mapstructure:"tls-max-version,omitempty" json:"tls-max-version,omitempty" yaml:"tls-max-version,omitempty"` 52 | TLSVersion string `mapstructure:"tls-version,omitempty" json:"tls-version,omitempty" yaml:"tls-version,omitempty"` 53 | Timeout time.Duration `mapstructure:"timeout,omitempty" json:"timeout,omitempty" yaml:"timeout,omitempty"` 54 | SkipVerify bool `mapstructure:"skip-verify,omitempty" json:"skip-verify,omitempty" yaml:"skip-verify,omitempty"` 55 | ProxyFromEnv bool `mapstructure:"proxy-from-env,omitempty" json:"proxy-from-env,omitempty" yaml:"proxy-from-env,omitempty"` 56 | Gzip bool `mapstructure:"gzip,omitempty" json:"gzip,omitempty" yaml:"gzip,omitempty"` 57 | MaxRcvMsgSize int `mapstructure:"max-rcv-msg-size,omitempty" json:"max-rcv-msg-size,omitempty" yaml:"max-rcv-msg-size,omitempty"` 58 | Format string `mapstructure:"format,omitempty" json:"format,omitempty" yaml:"format,omitempty"` 59 | Debug bool `mapstructure:"debug,omitempty" json:"debug,omitempty" yaml:"debug,omitempty"` 60 | // 61 | ElectionID string `mapstructure:"election-id,omitempty" json:"election-id,omitempty" yaml:"election-id,omitempty"` 62 | } 63 | 64 | type LocalFlags struct { 65 | // version 66 | UpgradeUsePkg bool 67 | // Get 68 | GetNetworkInstance string 69 | GetAFT string 70 | // flush 71 | FlushNetworkInstance string 72 | FlushNetworkInstanceAll bool 73 | FlushElectionIDOverride bool 74 | 75 | // modify redundancy 76 | // ModifySessionRedundancyAllPrimary bool 77 | ModifySessionRedundancySinglePrimary bool 78 | // modify persistence 79 | ModifySessionPersistancePreserve bool 80 | // modify ack 81 | // ModifySessionRibAck bool 82 | ModifySessionRibFibAck bool 83 | // modify operations 84 | ModifyInputFile string 85 | ModifyInputVarsFile string 86 | 87 | // workflow 88 | WorkflowFile string 89 | WorkflowInputVarsFile string 90 | } 91 | 92 | func New() *Config { 93 | return &Config{ 94 | GlobalFlags{}, 95 | LocalFlags{}, 96 | viper.NewWithOptions(viper.KeyDelimiter("/")), 97 | nil, 98 | nil, 99 | nil, 100 | nil, 101 | nil, 102 | nil, 103 | } 104 | } 105 | 106 | func (c *Config) SetLogger() { 107 | logger := log.StandardLogger() 108 | if c.Debug { 109 | logger.SetLevel(log.DebugLevel) 110 | } 111 | c.logger = log.NewEntry(logger) 112 | } 113 | 114 | func (c *Config) Load(ctx context.Context) error { 115 | c.FileConfig.SetEnvPrefix(envPrefix) 116 | c.FileConfig.SetEnvKeyReplacer(strings.NewReplacer("/", "_", "-", "_")) 117 | c.FileConfig.AutomaticEnv() 118 | if c.GlobalFlags.CfgFile != "" { 119 | c.FileConfig.SetConfigFile(c.GlobalFlags.CfgFile) 120 | configBytes, err := utils.ReadFile(ctx, c.FileConfig.ConfigFileUsed()) 121 | if err != nil { 122 | return err 123 | } 124 | err = c.FileConfig.ReadConfig(bytes.NewBuffer(configBytes)) 125 | if err != nil { 126 | return err 127 | } 128 | } else { 129 | home, err := homedir.Dir() 130 | if err != nil { 131 | return err 132 | } 133 | c.FileConfig.AddConfigPath(".") 134 | c.FileConfig.AddConfigPath(home) 135 | c.FileConfig.AddConfigPath(xdg.ConfigHome) 136 | c.FileConfig.AddConfigPath(xdg.ConfigHome + "/gribic") 137 | c.FileConfig.SetConfigName(configName) 138 | } 139 | 140 | err := c.FileConfig.ReadInConfig() 141 | if err != nil { 142 | return err 143 | } 144 | 145 | err = c.FileConfig.Unmarshal(c.FileConfig) 146 | if err != nil { 147 | return err 148 | } 149 | // c.mergeEnvVars() 150 | // return c.expandOSPathFlagValues() 151 | return nil 152 | } 153 | 154 | func (c *Config) SetPersistentFlagsFromFile(cmd *cobra.Command) { 155 | // set debug and log values from file before other persistant flags 156 | cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { 157 | if f.Name == "debug" || f.Name == "log" { 158 | if !f.Changed && c.FileConfig.IsSet(f.Name) { 159 | c.setFlagValue(cmd, f.Name, c.FileConfig.Get(f.Name)) 160 | } 161 | } 162 | }) 163 | // 164 | cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { 165 | if f.Name == "debug" || f.Name == "log" { 166 | return 167 | } 168 | if c.Debug { 169 | c.logger.Printf("cmd=%s, flagName=%s, changed=%v, isSetInFile=%v", 170 | cmd.Name(), f.Name, f.Changed, c.FileConfig.IsSet(f.Name)) 171 | } 172 | if !f.Changed && c.FileConfig.IsSet(f.Name) { 173 | c.setFlagValue(cmd, f.Name, c.FileConfig.Get(f.Name)) 174 | } 175 | }) 176 | } 177 | 178 | func (c *Config) SetLocalFlagsFromFile(cmd *cobra.Command) { 179 | cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { 180 | flagName := fmt.Sprintf("%s-%s", cmd.Name(), f.Name) 181 | if c.Debug { 182 | c.logger.Printf("cmd=%s, flagName=%s, changed=%v, isSetInFile=%v", 183 | cmd.Name(), f.Name, f.Changed, c.FileConfig.IsSet(flagName)) 184 | } 185 | if !f.Changed && c.FileConfig.IsSet(flagName) { 186 | c.setFlagValue(cmd, f.Name, c.FileConfig.Get(flagName)) 187 | } 188 | }) 189 | } 190 | 191 | func (c *Config) setFlagValue(cmd *cobra.Command, fName string, val interface{}) { 192 | switch val := val.(type) { 193 | case []interface{}: 194 | if c.Debug { 195 | c.logger.Printf("cmd=%s, flagName=%s, valueType=%T, length=%d, value=%#v", 196 | cmd.Name(), fName, val, len(val), val) 197 | } 198 | nVal := make([]string, 0, len(val)) 199 | for _, v := range val { 200 | nVal = append(nVal, fmt.Sprintf("%v", v)) 201 | } 202 | cmd.Flags().Set(fName, strings.Join(nVal, ",")) 203 | default: 204 | if c.Debug { 205 | c.logger.Printf("cmd=%s, flagName=%s, valueType=%T, value=%#v", 206 | cmd.Name(), fName, val, val) 207 | } 208 | cmd.Flags().Set(fName, fmt.Sprintf("%v", val)) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /config/modify_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_sortOperationsDRA(t *testing.T) { 9 | type args struct { 10 | ops []*OperationConfig 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want []*OperationConfig 16 | }{ 17 | { 18 | name: "empty", 19 | args: args{ 20 | ops: []*OperationConfig{}, 21 | }, 22 | want: []*OperationConfig{}, 23 | }, 24 | { 25 | // sort based on operation type 26 | name: "delete_replace_add", 27 | args: args{ 28 | ops: []*OperationConfig{ 29 | { 30 | Operation: "add", 31 | }, 32 | { 33 | Operation: "delete", 34 | }, 35 | { 36 | Operation: "replace", 37 | }, 38 | { 39 | Operation: "delete", 40 | }, 41 | }, 42 | }, 43 | want: []*OperationConfig{ 44 | { 45 | Operation: "delete", 46 | }, 47 | { 48 | Operation: "delete", 49 | }, 50 | { 51 | Operation: "replace", 52 | }, 53 | { 54 | Operation: "add", 55 | }, 56 | }, 57 | }, 58 | { 59 | // sort deletes based on type 60 | name: "sort_deletes", 61 | args: args{ 62 | ops: []*OperationConfig{ 63 | { 64 | Operation: "delete", 65 | NH: new(nhEntry), 66 | }, 67 | { 68 | Operation: "delete", 69 | NHG: new(nhgEntry), 70 | }, 71 | { 72 | Operation: "delete", 73 | IPv4: new(ipv4v6Entry), 74 | }, 75 | { 76 | Operation: "delete", 77 | IPv6: new(ipv4v6Entry), 78 | }, 79 | }, 80 | }, 81 | want: []*OperationConfig{ 82 | { 83 | Operation: "delete", 84 | IPv4: new(ipv4v6Entry), 85 | }, 86 | { 87 | Operation: "delete", 88 | IPv6: new(ipv4v6Entry), 89 | }, 90 | { 91 | Operation: "delete", 92 | NHG: new(nhgEntry), 93 | }, 94 | { 95 | Operation: "delete", 96 | NH: new(nhEntry), 97 | }, 98 | }, 99 | }, 100 | { 101 | // sort replaces based on type 102 | name: "sort_replaces", 103 | args: args{ 104 | ops: []*OperationConfig{ 105 | { 106 | Operation: "replace", 107 | NH: new(nhEntry), 108 | }, 109 | { 110 | Operation: "replace", 111 | IPv4: new(ipv4v6Entry), 112 | }, 113 | { 114 | Operation: "replace", 115 | NHG: new(nhgEntry), 116 | }, 117 | { 118 | Operation: "replace", 119 | IPv6: new(ipv4v6Entry), 120 | }, 121 | }, 122 | }, 123 | want: []*OperationConfig{ 124 | { 125 | Operation: "replace", 126 | NH: new(nhEntry), 127 | }, 128 | { 129 | Operation: "replace", 130 | NHG: new(nhgEntry), 131 | }, 132 | { 133 | Operation: "replace", 134 | IPv4: new(ipv4v6Entry), 135 | }, 136 | { 137 | Operation: "replace", 138 | IPv6: new(ipv4v6Entry), 139 | }, 140 | }, 141 | }, 142 | { 143 | // sort adds based on type 144 | name: "sort_adds", 145 | args: args{ 146 | ops: []*OperationConfig{ 147 | { 148 | Operation: "add", 149 | NH: new(nhEntry), 150 | }, 151 | { 152 | Operation: "add", 153 | IPv4: new(ipv4v6Entry), 154 | }, 155 | { 156 | Operation: "add", 157 | NHG: new(nhgEntry), 158 | }, 159 | { 160 | Operation: "add", 161 | IPv6: new(ipv4v6Entry), 162 | }, 163 | }, 164 | }, 165 | want: []*OperationConfig{ 166 | { 167 | Operation: "add", 168 | NH: new(nhEntry), 169 | }, 170 | { 171 | Operation: "add", 172 | NHG: new(nhgEntry), 173 | }, 174 | { 175 | Operation: "add", 176 | IPv4: new(ipv4v6Entry), 177 | }, 178 | { 179 | Operation: "add", 180 | IPv6: new(ipv4v6Entry), 181 | }, 182 | }, 183 | }, 184 | { 185 | // sort deletes and adds based on type 186 | name: "sort_deletes_and_adds", 187 | args: args{ 188 | ops: []*OperationConfig{ 189 | { 190 | Operation: "add", 191 | NH: new(nhEntry), 192 | }, 193 | { 194 | Operation: "add", 195 | IPv4: new(ipv4v6Entry), 196 | }, 197 | { 198 | Operation: "add", 199 | NHG: new(nhgEntry), 200 | }, 201 | { 202 | Operation: "add", 203 | IPv6: new(ipv4v6Entry), 204 | }, 205 | { 206 | Operation: "delete", 207 | NH: new(nhEntry), 208 | }, 209 | { 210 | Operation: "delete", 211 | NHG: new(nhgEntry), 212 | }, 213 | { 214 | Operation: "delete", 215 | IPv4: new(ipv4v6Entry), 216 | }, 217 | { 218 | Operation: "delete", 219 | IPv6: new(ipv4v6Entry), 220 | }, 221 | }, 222 | }, 223 | want: []*OperationConfig{ 224 | { 225 | Operation: "delete", 226 | IPv4: new(ipv4v6Entry), 227 | }, 228 | { 229 | Operation: "delete", 230 | IPv6: new(ipv4v6Entry), 231 | }, 232 | { 233 | Operation: "delete", 234 | NHG: new(nhgEntry), 235 | }, 236 | { 237 | Operation: "delete", 238 | NH: new(nhEntry), 239 | }, 240 | { 241 | Operation: "add", 242 | NH: new(nhEntry), 243 | }, 244 | { 245 | Operation: "add", 246 | NHG: new(nhgEntry), 247 | }, 248 | { 249 | Operation: "add", 250 | IPv4: new(ipv4v6Entry), 251 | }, 252 | { 253 | Operation: "add", 254 | IPv6: new(ipv4v6Entry), 255 | }, 256 | }, 257 | }, 258 | { 259 | // sort deletes, replaces and adds based on type 260 | name: "sort_deletes_replaces_and_adds", 261 | args: args{ 262 | ops: []*OperationConfig{ 263 | { 264 | Operation: "add", 265 | NH: new(nhEntry), 266 | }, 267 | { 268 | Operation: "add", 269 | IPv4: new(ipv4v6Entry), 270 | }, 271 | { 272 | Operation: "add", 273 | NHG: new(nhgEntry), 274 | }, 275 | { 276 | Operation: "add", 277 | IPv6: new(ipv4v6Entry), 278 | }, 279 | { 280 | Operation: "delete", 281 | NH: new(nhEntry), 282 | }, 283 | { 284 | Operation: "delete", 285 | NHG: new(nhgEntry), 286 | }, 287 | { 288 | Operation: "delete", 289 | IPv4: new(ipv4v6Entry), 290 | }, 291 | { 292 | Operation: "delete", 293 | IPv6: new(ipv4v6Entry), 294 | }, 295 | { 296 | Operation: "replace", 297 | NH: new(nhEntry), 298 | }, 299 | { 300 | Operation: "replace", 301 | IPv4: new(ipv4v6Entry), 302 | }, 303 | { 304 | Operation: "replace", 305 | NHG: new(nhgEntry), 306 | }, 307 | { 308 | Operation: "replace", 309 | IPv6: new(ipv4v6Entry), 310 | }, 311 | }, 312 | }, 313 | want: []*OperationConfig{ 314 | { 315 | Operation: "delete", 316 | IPv4: new(ipv4v6Entry), 317 | }, 318 | { 319 | Operation: "delete", 320 | IPv6: new(ipv4v6Entry), 321 | }, 322 | { 323 | Operation: "delete", 324 | NHG: new(nhgEntry), 325 | }, 326 | { 327 | Operation: "delete", 328 | NH: new(nhEntry), 329 | }, 330 | { 331 | Operation: "replace", 332 | NH: new(nhEntry), 333 | }, 334 | { 335 | Operation: "replace", 336 | NHG: new(nhgEntry), 337 | }, 338 | { 339 | Operation: "replace", 340 | IPv4: new(ipv4v6Entry), 341 | }, 342 | { 343 | Operation: "replace", 344 | IPv6: new(ipv4v6Entry), 345 | }, 346 | { 347 | Operation: "add", 348 | NH: new(nhEntry), 349 | }, 350 | { 351 | Operation: "add", 352 | NHG: new(nhgEntry), 353 | }, 354 | { 355 | Operation: "add", 356 | IPv4: new(ipv4v6Entry), 357 | }, 358 | { 359 | Operation: "add", 360 | IPv6: new(ipv4v6Entry), 361 | }, 362 | }, 363 | }, 364 | } 365 | for _, tt := range tests { 366 | t.Run(tt.name, func(t *testing.T) { 367 | if sortOperationsDRA(tt.args.ops); !reflect.DeepEqual(tt.args.ops, tt.want) { 368 | t.Errorf("sortOperations() = %v, want %v", tt.args.ops, tt.want) 369 | } 370 | }) 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /config/target.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "strings" 12 | "time" 13 | 14 | "github.com/mitchellh/mapstructure" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/credentials" 17 | "google.golang.org/grpc/credentials/insecure" 18 | "google.golang.org/grpc/encoding/gzip" 19 | ) 20 | 21 | const ( 22 | defaultNetworkInstanceName = "default" 23 | ) 24 | 25 | type TargetConfig struct { 26 | Name string `json:"name,omitempty" mapstructure:"name,omitempty"` 27 | Address string `json:"address,omitempty" mapstructure:"address,omitempty"` 28 | DefaultNI string `json:"default-ni,omitempty" mapstructure:"default-ni,omitempty"` 29 | Insecure *bool `json:"insecure,omitempty" mapstructure:"insecure,omitempty"` 30 | SkipVerify *bool `json:"skip-verify,omitempty" mapstructure:"skip-verify,omitempty"` 31 | Username *string `json:"username,omitempty" mapstructure:"username,omitempty"` 32 | Password *string `json:"password,omitempty" mapstructure:"password,omitempty"` 33 | Timeout time.Duration `json:"timeout,omitempty" mapstructure:"timeout,omitempty"` 34 | TLSCert *string `json:"tls-cert,omitempty" mapstructure:"tls-cert,omitempty"` 35 | TLSKey *string `json:"tls-key,omitempty" mapstructure:"tls-key,omitempty"` 36 | TLSCA *string `json:"tlsca,omitempty" mapstructure:"tlsca,omitempty"` 37 | TLSMinVersion string `json:"tls-min-version,omitempty" mapstructure:"tls-min-version,omitempty"` 38 | TLSMaxVersion string `json:"tls-max-version,omitempty" mapstructure:"tls-max-version,omitempty"` 39 | TLSVersion string `json:"tls-version,omitempty" mapstructure:"tls-version,omitempty"` 40 | Gzip *bool `json:"gzip,omitempty" mapstructure:"gzip,omitempty"` 41 | MaxRcvMsgSize int `json:"max-rcv-msg-size,omitempty" mapstructure:"max-rcv-msg-size,omitempty"` 42 | // modify RPC session params 43 | // Redundancy string `json:"redundancy,omitempty" mapstructure:"redundancy,omitempty"` 44 | // Persistence string `json:"persistence,omitempty" mapstructure:"persistence,omitempty"` 45 | // AckType string `json:"ack-type,omitempty" mapstructure:"ack-type,omitempty"` 46 | } 47 | 48 | func (c *Config) GetTargets() (map[string]*TargetConfig, error) { 49 | targetsConfigs := make(map[string]*TargetConfig) 50 | if len(c.Address) > 0 { 51 | var err error 52 | for _, addr := range c.Address { 53 | tc := new(TargetConfig) 54 | err = c.parseAddress(tc, addr) 55 | if err != nil { 56 | return nil, fmt.Errorf("%q failed to parse address: %v", addr, err) 57 | } 58 | c.setTargetConfigDefaults(tc) 59 | targetsConfigs[tc.Name] = tc 60 | c.logger.Debugf("%q target-config: %s", addr, tc) 61 | } 62 | return targetsConfigs, nil 63 | } 64 | targetsMap := c.FileConfig.GetStringMap("targets") 65 | if len(targetsMap) == 0 { 66 | return nil, errors.New("no targets found") 67 | } 68 | for addr, t := range targetsMap { 69 | tc := new(TargetConfig) 70 | switch t := t.(type) { 71 | case map[string]interface{}: 72 | decoder, err := mapstructure.NewDecoder( 73 | &mapstructure.DecoderConfig{ 74 | DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 75 | Result: tc, 76 | }, 77 | ) 78 | if err != nil { 79 | return nil, err 80 | } 81 | err = decoder.Decode(t) 82 | if err != nil { 83 | return nil, err 84 | } 85 | case nil: 86 | default: 87 | return nil, fmt.Errorf("unexpected targets format, got a %T", t) 88 | } 89 | err := c.parseAddress(tc, addr) 90 | if err != nil { 91 | return nil, fmt.Errorf("%q failed to parse address: %v", addr, err) 92 | } 93 | c.setTargetConfigDefaults(tc) 94 | targetsConfigs[tc.Name] = tc 95 | c.logger.Debugf("%q target-config: %s", addr, tc) 96 | } 97 | return targetsConfigs, nil 98 | } 99 | 100 | func (c *Config) parseAddress(tc *TargetConfig, addr string) error { 101 | _, _, err := net.SplitHostPort(addr) 102 | if err != nil { 103 | if strings.Contains(err.Error(), "missing port in address") || 104 | strings.Contains(err.Error(), "too many colons in address") { 105 | tc.Address = net.JoinHostPort(addr, c.Port) 106 | return nil 107 | } 108 | return fmt.Errorf("error parsing address %q: %v", addr, err) 109 | 110 | } 111 | tc.Address = addr 112 | return nil 113 | } 114 | 115 | func (c *Config) setTargetConfigDefaults(tc *TargetConfig) { 116 | if tc.Name == "" { 117 | tc.Name = tc.Address 118 | } 119 | if tc.DefaultNI == "" { 120 | tc.DefaultNI = defaultNetworkInstanceName 121 | } 122 | if c.Insecure { 123 | tc.Insecure = &c.Insecure 124 | } 125 | if tc.Timeout <= 0 { 126 | tc.Timeout = c.Timeout 127 | } 128 | if tc.Username == nil { 129 | tc.Username = &c.Username 130 | } 131 | if tc.Password == nil { 132 | tc.Password = &c.Password 133 | } 134 | if tc.SkipVerify == nil { 135 | tc.SkipVerify = &c.SkipVerify 136 | } 137 | if tc.Insecure == nil || (tc.Insecure != nil && !*tc.Insecure) { 138 | if tc.TLSCA == nil { 139 | if c.TLSCa != "" { 140 | tc.TLSCA = &c.TLSCa 141 | } 142 | } 143 | if tc.TLSCert == nil { 144 | tc.TLSCert = &c.TLSCert 145 | } 146 | if tc.TLSKey == nil { 147 | tc.TLSKey = &c.TLSKey 148 | } 149 | } 150 | if tc.TLSVersion == "" { 151 | tc.TLSVersion = c.TLSVersion 152 | } 153 | if tc.TLSMinVersion == "" { 154 | tc.TLSMinVersion = c.TLSMinVersion 155 | } 156 | if tc.TLSMaxVersion == "" { 157 | tc.TLSMaxVersion = c.TLSMaxVersion 158 | } 159 | if tc.Gzip == nil { 160 | tc.Gzip = &c.Gzip 161 | } 162 | if tc.MaxRcvMsgSize == 0 { 163 | tc.MaxRcvMsgSize = c.MaxRcvMsgSize 164 | } 165 | } 166 | 167 | func (tc *TargetConfig) DialOpts() ([]grpc.DialOption, error) { 168 | tOpts := make([]grpc.DialOption, 0) 169 | if tc.Insecure != nil && *tc.Insecure { 170 | tOpts = append(tOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) 171 | } else { 172 | tlsConfig, err := tc.newTLS() 173 | if err != nil { 174 | return nil, err 175 | } 176 | tOpts = append(tOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) 177 | } 178 | if tc.Gzip != nil && *tc.Gzip { 179 | tOpts = append(tOpts, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name))) 180 | } 181 | if tc.MaxRcvMsgSize > 0 { 182 | tOpts = append(tOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(tc.MaxRcvMsgSize))) 183 | } 184 | return tOpts, nil 185 | } 186 | 187 | func (tc *TargetConfig) newTLS() (*tls.Config, error) { 188 | tlsConfig := &tls.Config{ 189 | Renegotiation: tls.RenegotiateNever, 190 | InsecureSkipVerify: *tc.SkipVerify, 191 | MaxVersion: tc.getTLSMaxVersion(), 192 | MinVersion: tc.getTLSMinVersion(), 193 | } 194 | err := loadCerts(tlsConfig, tc) 195 | if err != nil { 196 | return nil, err 197 | } 198 | return tlsConfig, nil 199 | } 200 | 201 | func (tc *TargetConfig) getTLSMinVersion() uint16 { 202 | v := tlsVersionStringToUint(tc.TLSVersion) 203 | if v > 0 { 204 | return v 205 | } 206 | return tlsVersionStringToUint(tc.TLSMinVersion) 207 | } 208 | 209 | func (tc *TargetConfig) getTLSMaxVersion() uint16 { 210 | v := tlsVersionStringToUint(tc.TLSVersion) 211 | if v > 0 { 212 | return v 213 | } 214 | return tlsVersionStringToUint(tc.TLSMaxVersion) 215 | } 216 | 217 | func tlsVersionStringToUint(v string) uint16 { 218 | switch v { 219 | default: 220 | return 0 221 | case "1.3": 222 | return tls.VersionTLS13 223 | case "1.2": 224 | return tls.VersionTLS12 225 | case "1.1": 226 | return tls.VersionTLS11 227 | case "1.0", "1": 228 | return tls.VersionTLS10 229 | } 230 | } 231 | 232 | func loadCerts(tlscfg *tls.Config, tc *TargetConfig) error { 233 | if *tc.TLSCert != "" && *tc.TLSKey != "" { 234 | certificate, err := tls.LoadX509KeyPair(*tc.TLSCert, *tc.TLSKey) 235 | if err != nil { 236 | return err 237 | } 238 | tlscfg.Certificates = []tls.Certificate{certificate} 239 | // tlscfg.BuildNameToCertificate() 240 | } 241 | if tc.TLSCA != nil && *tc.TLSCA != "" { 242 | certPool := x509.NewCertPool() 243 | caFile, err := ioutil.ReadFile(*tc.TLSCA) 244 | if err != nil { 245 | return err 246 | } 247 | if ok := certPool.AppendCertsFromPEM(caFile); !ok { 248 | return errors.New("failed to append certificate") 249 | } 250 | tlscfg.RootCAs = certPool 251 | } 252 | return nil 253 | } 254 | 255 | func (tc *TargetConfig) String() string { 256 | b, err := json.Marshal(tc) 257 | if err != nil { 258 | return "" 259 | } 260 | return string(b) 261 | } 262 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karimra/gribic 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/adrg/xdg v0.4.0 7 | github.com/karimra/gnmic v0.26.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/mitchellh/mapstructure v1.5.0 10 | github.com/openconfig/gnmi v0.10.0 11 | github.com/openconfig/gribi v1.0.0 12 | github.com/openconfig/gribigo v0.0.0-20220216214442-0aae099db56f 13 | github.com/prometheus/client_golang v1.14.0 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/spf13/cobra v1.6.1 16 | github.com/spf13/pflag v1.0.5 17 | github.com/spf13/viper v1.10.1 18 | golang.org/x/sync v0.3.0 19 | google.golang.org/grpc v1.59.0 20 | google.golang.org/protobuf v1.31.0 21 | ) 22 | 23 | require ( 24 | cloud.google.com/go v0.110.7 // indirect 25 | cloud.google.com/go/compute v1.23.0 // indirect 26 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 27 | cloud.google.com/go/iam v1.1.1 // indirect 28 | github.com/Masterminds/goutils v1.1.1 // indirect 29 | github.com/Microsoft/go-winio v0.5.2 // indirect 30 | github.com/ProtonMail/go-crypto v0.0.0-20220517143526-88bb52951d5b // indirect 31 | github.com/Shopify/ejson v1.3.3 // indirect 32 | github.com/acomagu/bufpipe v1.0.3 // indirect 33 | github.com/apparentlymart/go-cidr v1.1.0 // indirect 34 | github.com/armon/go-metrics v0.4.0 // indirect 35 | github.com/armon/go-radix v1.0.0 // indirect 36 | github.com/aws/aws-sdk-go v1.44.206 // indirect 37 | github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect 38 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect 39 | github.com/aws/aws-sdk-go-v2/config v1.15.9 // indirect 40 | github.com/aws/aws-sdk-go-v2/credentials v1.12.4 // indirect 41 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 // indirect 42 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14 // indirect 43 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect 44 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect 45 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect 46 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2 // indirect 47 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 // indirect 48 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6 // indirect 49 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect 50 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5 // indirect 51 | github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10 // indirect 52 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 // indirect 53 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect 54 | github.com/aws/smithy-go v1.11.2 // indirect 55 | github.com/beorn7/perks v1.0.1 // indirect 56 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 57 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 58 | github.com/docker/libkv v0.2.2-0.20180912205406-458977154600 // indirect 59 | github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect 60 | github.com/emirpasic/gods v1.18.1 // indirect 61 | github.com/fatih/color v1.13.0 // indirect 62 | github.com/frankban/quicktest v1.14.2 // indirect 63 | github.com/fsnotify/fsnotify v1.6.0 // indirect 64 | github.com/go-git/gcfg v1.5.0 // indirect 65 | github.com/go-git/go-billy/v5 v5.3.1 // indirect 66 | github.com/go-git/go-git/v5 v5.4.2 // indirect 67 | github.com/golang/glog v1.1.2 // indirect 68 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 69 | github.com/golang/protobuf v1.5.3 // indirect 70 | github.com/golang/snappy v0.0.4 // indirect 71 | github.com/google/go-cmp v0.5.9 // indirect 72 | github.com/google/s2a-go v0.1.4 // indirect 73 | github.com/google/uuid v1.3.1 // indirect 74 | github.com/google/wire v0.5.0 // indirect 75 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 76 | github.com/googleapis/gax-go/v2 v2.11.0 // indirect 77 | github.com/gosimple/slug v1.12.0 // indirect 78 | github.com/gosimple/unidecode v1.0.1 // indirect 79 | github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047 // indirect 80 | github.com/hairyhenderson/gomplate/v3 v3.11.4 // indirect 81 | github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf // indirect 82 | github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce // indirect 83 | github.com/hashicorp/consul/api v1.19.1 // indirect 84 | github.com/hashicorp/errwrap v1.1.0 // indirect 85 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 86 | github.com/hashicorp/go-hclog v1.2.0 // indirect 87 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 88 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 89 | github.com/hashicorp/go-multierror v1.1.1 // indirect 90 | github.com/hashicorp/go-plugin v1.4.4 // indirect 91 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 92 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 93 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect 94 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.5 // indirect 95 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 96 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 97 | github.com/hashicorp/go-uuid v1.0.3 // indirect 98 | github.com/hashicorp/go-version v1.6.0 // indirect 99 | github.com/hashicorp/golang-lru v0.5.4 // indirect 100 | github.com/hashicorp/hcl v1.0.0 // indirect 101 | github.com/hashicorp/serf v0.10.1 // indirect 102 | github.com/hashicorp/vault/api v1.6.0 // indirect 103 | github.com/hashicorp/vault/sdk v0.5.0 // indirect 104 | github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect 105 | github.com/imdario/mergo v0.3.13 // indirect 106 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 107 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 108 | github.com/jhump/protoreflect v1.12.0 // indirect 109 | github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067 // indirect 110 | github.com/jmespath/go-jmespath v0.4.0 // indirect 111 | github.com/joho/godotenv v1.4.0 // indirect 112 | github.com/kevinburke/ssh_config v1.2.0 // indirect 113 | github.com/kr/fs v0.1.0 // indirect 114 | github.com/kylelemons/godebug v1.1.0 // indirect 115 | github.com/magiconair/properties v1.8.5 // indirect 116 | github.com/mattn/go-colorable v0.1.12 // indirect 117 | github.com/mattn/go-isatty v0.0.14 // indirect 118 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 119 | github.com/miekg/dns v1.1.49 // indirect 120 | github.com/mitchellh/copystructure v1.2.0 // indirect 121 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 122 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 123 | github.com/oklog/run v1.1.0 // indirect 124 | github.com/openconfig/goyang v1.2.0 // indirect 125 | github.com/pelletier/go-toml v1.9.4 // indirect 126 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 127 | github.com/pkg/errors v0.9.1 // indirect 128 | github.com/pkg/sftp v1.13.4 // indirect 129 | github.com/prometheus/client_model v0.3.0 // indirect 130 | github.com/prometheus/common v0.37.0 // indirect 131 | github.com/prometheus/procfs v0.8.0 // indirect 132 | github.com/rogpeppe/go-internal v1.9.0 // indirect 133 | github.com/rs/zerolog v1.26.1 // indirect 134 | github.com/ryanuber/go-glob v1.0.0 // indirect 135 | github.com/sergi/go-diff v1.2.0 // indirect 136 | github.com/spf13/afero v1.8.2 // indirect 137 | github.com/spf13/cast v1.4.1 // indirect 138 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 139 | github.com/subosito/gotenv v1.2.0 // indirect 140 | github.com/ugorji/go/codec v1.2.7 // indirect 141 | github.com/xanzy/ssh-agent v0.3.1 // indirect 142 | github.com/zealic/xignore v0.3.3 // indirect 143 | go.etcd.io/bbolt v1.3.6 // indirect 144 | go.opencensus.io v0.24.0 // indirect 145 | go.uber.org/atomic v1.9.0 // indirect 146 | go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect 147 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect 148 | gocloud.dev v0.25.1-0.20220408200107-09b10f7359f7 // indirect 149 | golang.org/x/crypto v0.12.0 // indirect 150 | golang.org/x/net v0.14.0 // indirect 151 | golang.org/x/oauth2 v0.11.0 // indirect 152 | golang.org/x/sys v0.11.0 // indirect 153 | golang.org/x/text v0.12.0 // indirect 154 | golang.org/x/time v0.3.0 // indirect 155 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 156 | google.golang.org/api v0.126.0 // indirect 157 | google.golang.org/appengine v1.6.7 // indirect 158 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect 159 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect 160 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 161 | gopkg.in/ini.v1 v1.66.2 // indirect 162 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 163 | gopkg.in/warnings.v0 v0.1.2 // indirect 164 | inet.af/netaddr v0.0.0-20220811202034-502d2d690317 // indirect 165 | k8s.io/client-go v0.24.1 // indirect 166 | ) 167 | 168 | require ( 169 | cloud.google.com/go/storage v1.30.1 // indirect 170 | github.com/openconfig/ygot v0.15.1 171 | gopkg.in/yaml.v2 v2.4.0 172 | ) 173 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # The install script is based off of the Apache 2.0 script from Helm, 4 | # https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 5 | 6 | : ${BINARY_NAME:="gribic"} 7 | : ${PROJECT_NAME:="gribic"} # if project name does not match binary name 8 | : ${USE_SUDO:="true"} 9 | : ${USE_PKG:="false"} # default --use-pkg flag value. will use package installation by default unless the default is changed to false 10 | : ${VERIFY_CHECKSUM:="false"} 11 | : ${BIN_INSTALL_DIR:="/usr/local/bin"} 12 | : ${REPO_NAME:="karimra/gribic"} 13 | : ${REPO_URL:="https://github.com/$REPO_NAME"} 14 | : ${PROJECT_URL:="https://gribic.kmrd.dev"} 15 | : ${LATEST_URL:="https://api.github.com/repos/$REPO_NAME/releases/latest"} 16 | # detectArch discovers the architecture for this system. 17 | detectArch() { 18 | ARCH=$(uname -m) 19 | # case $ARCH in 20 | # armv5*) ARCH="armv5" ;; 21 | # armv6*) ARCH="armv6" ;; 22 | # armv7*) ARCH="arm" ;; 23 | # aarch64) ARCH="arm64" ;; 24 | # x86) ARCH="386" ;; 25 | # x86_64) ARCH="amd64" ;; 26 | # i686) ARCH="386" ;; 27 | # i386) ARCH="386" ;; 28 | # esac 29 | } 30 | 31 | # detectOS discovers the operating system for this system and its package format 32 | detectOS() { 33 | OS=$(echo $(uname) | tr '[:upper:]' '[:lower:]') 34 | 35 | case "$OS" in 36 | # Minimalist GNU for Windows 37 | mingw*) OS='windows' ;; 38 | esac 39 | 40 | if type "rpm" &>/dev/null; then 41 | PKG_FORMAT="rpm" 42 | elif type "dpkg" &>/dev/null; then 43 | PKG_FORMAT="deb" 44 | fi 45 | } 46 | 47 | # runs the given command as root (detects if we are root already) 48 | runAsRoot() { 49 | local CMD="$*" 50 | 51 | if [ $EUID -ne 0 -a $USE_SUDO = "true" ]; then 52 | CMD="sudo $CMD" 53 | fi 54 | 55 | $CMD 56 | } 57 | 58 | # verifySupported checks that the os/arch combination is supported 59 | verifySupported() { 60 | local supported="darwin-x86_64\nlinux-i386\nlinux-x86_64\nlinux-armv7\nlinux-aarch64" 61 | if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then 62 | echo "No prebuilt binary for ${OS}-${ARCH}." 63 | echo "To build from source, go to ${REPO_URL}" 64 | exit 1 65 | fi 66 | 67 | if ! type "curl" &>/dev/null && ! type "wget" &>/dev/null; then 68 | echo "Either curl or wget is required" 69 | exit 1 70 | fi 71 | } 72 | 73 | # verifyOpenssl checks if openssl is installed to perform checksum operation 74 | verifyOpenssl() { 75 | if [ $VERIFY_CHECKSUM == "true" ]; then 76 | if ! type "openssl" &>/dev/null; then 77 | echo "openssl is not found. It is used to verify checksum of the downloaded file." 78 | exit 1 79 | fi 80 | fi 81 | } 82 | 83 | 84 | # setDesiredVersion sets the desired version either to an explicit version provided by a user 85 | # or to the latest release available on github releases 86 | setDesiredVersion() { 87 | if [ "x$DESIRED_VERSION" == "x" ]; then 88 | # when desired version is not provided 89 | # get latest tag from the gh releases 90 | local cmd="" 91 | if type "curl" &>/dev/null; then 92 | cmd="curl -s " 93 | elif type "wget" &>/dev/null; then 94 | cmd="wget -q -O- " 95 | else 96 | echo "Missing curl or wget utility to download the installation package" 97 | exit 1 98 | fi 99 | local latest_release_url="" 100 | # use jq to filter the api response if available 101 | if type "jq" &>/dev/null; then 102 | latest_release_url=$($cmd $LATEST_URL | jq -r .html_url) 103 | # else use grep and cut 104 | else 105 | latest_release_url=$($cmd $LATEST_URL | grep "html_url.*releases/tag" | cut -d '"' -f 4) 106 | fi 107 | if [ "x$latest_release_url" == "x" ]; then 108 | echo "Could not determine the latest release" 109 | exit 1 110 | fi 111 | TAG=$(echo $latest_release_url | cut -d '"' -f 2 | awk -F "/" '{print $NF}') 112 | # tag with stripped `v` prefix 113 | TAG_WO_VER=$(echo "${TAG}" | cut -c 2-) 114 | else 115 | TAG=$DESIRED_VERSION 116 | TAG_WO_VER=$(echo "${TAG}" | cut -c 2-) 117 | fi 118 | } 119 | 120 | # checkInstalledVersion checks which version is installed and 121 | # if it needs to be changed. 122 | checkInstalledVersion() { 123 | if [[ -f "${BIN_INSTALL_DIR}/${BINARY_NAME}" ]]; then 124 | local version=$("${BIN_INSTALL_DIR}/${BINARY_NAME}" version | grep version | awk '{print $NF}') 125 | if [[ "v$version" == "$TAG" ]]; then 126 | echo "${BINARY_NAME} is already at ${DESIRED_VERSION:-latest ($version)}" version 127 | return 0 128 | else 129 | echo "${BINARY_NAME} ${TAG_WO_VER} is available. Changing from version ${version}." 130 | return 1 131 | fi 132 | else 133 | return 1 134 | fi 135 | } 136 | 137 | # createTempDir creates temporary directory where we downloaded files 138 | createTempDir() { 139 | TMP_ROOT="$(mktemp -d)" 140 | TMP_BIN="$TMP_ROOT/$BINARY_NAME" 141 | } 142 | 143 | # downloadFile downloads the latest binary archive, the checksum file and performs the sum check 144 | downloadFile() { 145 | EXT="tar.gz" # download file extension 146 | if [ $USE_PKG == "true" ]; then 147 | if [ -z $PKG_FORMAT ]; then 148 | echo "Package for $OS-$ARCH is not available" 149 | cleanup 150 | exit 1 151 | fi 152 | EXT=$PKG_FORMAT 153 | fi 154 | ARCHIVE="${PROJECT_NAME}_${TAG_WO_VER}_${OS}_${ARCH}.${EXT}" 155 | DOWNLOAD_URL="${REPO_URL}/releases/download/${TAG}/${ARCHIVE}" 156 | CHECKSUM_URL="${REPO_URL}/releases/download/${TAG}/checksums.txt" 157 | TMP_FILE="$TMP_ROOT/$ARCHIVE" 158 | SUM_FILE="$TMP_ROOT/checksums.txt" 159 | echo "Downloading $DOWNLOAD_URL" 160 | if type "curl" &>/dev/null; then 161 | curl -SsL "$CHECKSUM_URL" -o "$SUM_FILE" 162 | curl -SsL "$DOWNLOAD_URL" -o "$TMP_FILE" 163 | elif type "wget" &>/dev/null; then 164 | wget -q -O "$SUM_FILE" "$CHECKSUM_URL" 165 | wget -q -O "$TMP_FILE" "$DOWNLOAD_URL" 166 | fi 167 | 168 | # verify downloaded file 169 | if [ $VERIFY_CHECKSUM == "true" ]; then 170 | local sum=$(openssl sha1 -sha256 ${TMP_FILE} | awk '{print $2}') 171 | local expected_sum=$(cat ${SUM_FILE} | grep -i $ARCHIVE | awk '{print $1}') 172 | if [ "$sum" != "$expected_sum" ]; then 173 | echo "SHA sum of ${TMP_FILE} does not match. Aborting." 174 | exit 1 175 | fi 176 | echo "Checksum verified" 177 | fi 178 | } 179 | 180 | # installFile verifies the SHA256 for the file, then unpacks and 181 | # installs it. By default, the installation is done from .tar.gz archive, that can be overriden with --use-pkg flag 182 | installFile() { 183 | tar xf "$TMP_FILE" -C "$TMP_ROOT" 184 | echo "Preparing to install $BINARY_NAME ${TAG_WO_VER} into ${BIN_INSTALL_DIR}" 185 | runAsRoot cp -f "$TMP_ROOT/$BINARY_NAME" "$BIN_INSTALL_DIR/$BINARY_NAME" 186 | echo "$BINARY_NAME installed into $BIN_INSTALL_DIR/$BINARY_NAME" 187 | } 188 | 189 | # installPkg installs the downloaded version of a package in a deb or rpm format 190 | installPkg() { 191 | echo "Preparing to install $BINARY_NAME ${TAG_WO_VER} from package" 192 | if [ $PKG_FORMAT == "deb" ]; then 193 | runAsRoot dpkg -i $TMP_FILE 194 | elif [ $PKG_FORMAT == "rpm" ]; then 195 | runAsRoot rpm -U $TMP_FILE 196 | fi 197 | } 198 | 199 | # fail_trap is executed if an error occurs. 200 | fail_trap() { 201 | result=$? 202 | if [ "$result" != "0" ]; then 203 | if [[ -n "$INPUT_ARGUMENTS" ]]; then 204 | echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS" 205 | help 206 | else 207 | echo "Failed to install $BINARY_NAME" 208 | fi 209 | echo -e "\tFor support, go to $REPO_URL/issues" 210 | fi 211 | cleanup 212 | exit $result 213 | } 214 | 215 | # testVersion tests the installed client to make sure it is working. 216 | testVersion() { 217 | set +e 218 | $BIN_INSTALL_DIR/$BINARY_NAME version 219 | if [ "$?" = "1" ]; then 220 | echo "$BINARY_NAME not found. Is $BIN_INSTALL_DIR in your "'$PATH?' 221 | exit 1 222 | fi 223 | set -e 224 | } 225 | 226 | # help provides possible cli installation arguments 227 | help() { 228 | echo "Accepted cli arguments are:" 229 | echo -e "\t[--help|-h ] ->> prints this help" 230 | echo -e "\t[--version|-v ] . When not defined it fetches the latest release from GitHub" 231 | echo -e "\te.g. --version v0.1.1" 232 | echo -e "\t[--use-pkg] ->> install from deb/rpm packages" 233 | echo -e "\t[--no-sudo] ->> install without sudo" 234 | echo -e "\t[--verify-checksum] ->> verify checksum of the downloaded file" 235 | } 236 | 237 | # removes temporary directory used to download artefacts 238 | cleanup() { 239 | if [[ -d "${TMP_ROOT:-}" ]]; then 240 | rm -rf "$TMP_ROOT" 241 | fi 242 | } 243 | 244 | # Execution 245 | 246 | #Stop execution on any error 247 | trap "fail_trap" EXIT 248 | set -e 249 | 250 | # Parsing input arguments (if any) 251 | export INPUT_ARGUMENTS="${@}" 252 | set -u 253 | while [[ $# -gt 0 ]]; do 254 | case $1 in 255 | '--version' | -v) 256 | shift 257 | if [[ $# -ne 0 ]]; then 258 | export DESIRED_VERSION="v${1}" 259 | else 260 | echo -e "Please provide the desired version. e.g. --version 0.1.1" 261 | exit 0 262 | fi 263 | ;; 264 | '--no-sudo') 265 | USE_SUDO="false" 266 | ;; 267 | '--verify-checksum') 268 | VERIFY_CHECKSUM="true" 269 | ;; 270 | '--use-pkg') 271 | USE_PKG="true" 272 | ;; 273 | '--help' | -h) 274 | help 275 | exit 0 276 | ;; 277 | *) 278 | exit 1 279 | ;; 280 | esac 281 | shift 282 | done 283 | set +u 284 | 285 | detectArch 286 | detectOS 287 | verifySupported 288 | setDesiredVersion 289 | if ! checkInstalledVersion; then 290 | createTempDir 291 | verifyOpenssl 292 | downloadFile 293 | if [ $USE_PKG == "true" ]; then 294 | installPkg 295 | else 296 | installFile 297 | fi 298 | testVersion 299 | cleanup 300 | fi 301 | -------------------------------------------------------------------------------- /app/modify.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | 9 | "github.com/karimra/gribic/api" 10 | "github.com/karimra/gribic/config" 11 | spb "github.com/openconfig/gribi/v1/proto/service" 12 | "github.com/spf13/cobra" 13 | "google.golang.org/protobuf/encoding/prototext" 14 | ) 15 | 16 | type modifyResponse struct { 17 | TargetError 18 | rsp *spb.ModifyResponse 19 | } 20 | 21 | func (a *App) InitModifyFlags(cmd *cobra.Command) { 22 | cmd.ResetFlags() 23 | // session redundancy 24 | // cmd.Flags().BoolVarP(&a.Config.ModifySessionRedundancyAllPrimary, "all-primary", "", false, "set session client redundancy to ALL_PRIMARY") 25 | cmd.Flags().BoolVarP(&a.Config.ModifySessionRedundancySinglePrimary, "single-primary", "", false, "set session client redundancy to SINGLE_PRIMARY") 26 | // session persistence 27 | // cmd.Flags().BoolVarP(&a.Config.ModifySessionPersistanceDelete, "delete", "", false, "set session persistence to DELETE") 28 | cmd.Flags().BoolVarP(&a.Config.ModifySessionPersistancePreserve, "preserve", "", false, "set session persistence to PRESERVE") 29 | // session ack 30 | // cmd.Flags().BoolVarP(&a.Config.ModifySessionRibAck, "rib", "", false, "set session ack type to RIB") 31 | cmd.Flags().BoolVarP(&a.Config.ModifySessionRibFibAck, "fib", "", false, "set session ack type to RIB_FIB") 32 | // modify input file 33 | cmd.Flags().StringVarP(&a.Config.ModifyInputFile, "input-file", "", "", "path to a file specifying the modify RPC input") 34 | } 35 | 36 | func (a *App) ModifyPreRunE(cmd *cobra.Command, args []string) error { 37 | // parse election ID 38 | var err error 39 | a.electionID, err = config.ParseUint128(a.Config.ElectionID) 40 | if err != nil { 41 | return err 42 | } 43 | if a.Config.ModifyInputFile == "" { 44 | return errors.New("missing --input-file value") 45 | } 46 | 47 | err = a.Config.ReadModifyFileTemplate() 48 | if err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | func (a *App) ModifyRunE(cmd *cobra.Command, args []string) error { 55 | targets, err := a.GetTargets() 56 | if err != nil { 57 | return err 58 | } 59 | a.Logger.Debugf("targets: %v", targets) 60 | numTargets := len(targets) 61 | responseChan := make(chan *modifyResponse, numTargets) 62 | a.wg.Add(numTargets) 63 | for _, t := range targets { 64 | go func(t *target) { 65 | defer a.wg.Done() 66 | // create context 67 | ctx, cancel := context.WithCancel(a.ctx) 68 | defer cancel() 69 | // append credentials to context 70 | ctx = appendCredentials(ctx, t.Config) 71 | // create a grpc conn 72 | err = a.CreateGrpcClient(ctx, t, a.createBaseDialOpts()...) 73 | if err != nil { 74 | responseChan <- &modifyResponse{ 75 | TargetError: TargetError{ 76 | TargetName: t.Config.Address, 77 | Err: err, 78 | }, 79 | } 80 | return 81 | } 82 | defer t.Close() 83 | rspCh := a.gribiModify(ctx, t) 84 | for { 85 | select { 86 | case rsp, ok := <-rspCh: 87 | if !ok { 88 | return 89 | } 90 | if rsp != nil { 91 | if rsp.TargetError.Err != nil { 92 | a.Logger.Errorf("%+v", rsp.TargetError) 93 | } else { 94 | a.Logger.Printf("%s\nresponse: %s", rsp.TargetError.TargetName, prototext.Format(rsp.rsp)) 95 | } 96 | 97 | } 98 | case <-ctx.Done(): 99 | a.Logger.Print(ctx.Err()) 100 | } 101 | } 102 | }(t) 103 | } 104 | // 105 | a.wg.Wait() 106 | close(responseChan) 107 | 108 | return nil 109 | } 110 | 111 | func (a *App) gribiModify(ctx context.Context, t *target) chan *modifyResponse { 112 | rspCh := make(chan *modifyResponse) 113 | t.gRIBIClient = spb.NewGRIBIClient(t.conn) 114 | 115 | go func() { 116 | defer func() { 117 | close(rspCh) 118 | a.Logger.Infof("target %s modify stream done", t.Config.Name) 119 | }() 120 | // create client 121 | modClient, err := t.gRIBIClient.Modify(ctx) 122 | if err != nil { 123 | rspCh <- &modifyResponse{ 124 | TargetError: TargetError{ 125 | TargetName: t.Config.Name, 126 | Err: err, 127 | }, 128 | } 129 | return 130 | } 131 | modifyInput, err := a.Config.GenerateModifyInputs(t.Config.Name) 132 | if err != nil { 133 | rspCh <- &modifyResponse{ 134 | TargetError: TargetError{ 135 | TargetName: t.Config.Name, 136 | Err: err, 137 | }, 138 | } 139 | return 140 | } 141 | 142 | // session parameters & election ID 143 | modParams, err := a.createModifyRequestParams(modifyInput) 144 | if err != nil { 145 | rspCh <- &modifyResponse{ 146 | TargetError: TargetError{ 147 | TargetName: t.Config.Name, 148 | Err: err, 149 | }, 150 | } 151 | return 152 | } 153 | switch len(modParams) { 154 | case 1: // no election ID (all-primary) 155 | a.Logger.Printf("sending request=%v to %q", modParams[0], t.Config.Name) 156 | err = modClient.Send(modParams[0]) 157 | if err != nil { 158 | rspCh <- &modifyResponse{ 159 | TargetError: TargetError{ 160 | TargetName: t.Config.Name, 161 | Err: err, 162 | }, 163 | } 164 | return 165 | } 166 | modRsp, err := modClient.Recv() 167 | rspCh <- &modifyResponse{ 168 | TargetError: TargetError{ 169 | TargetName: t.Config.Name, 170 | Err: err, 171 | }, 172 | rsp: modRsp, 173 | } 174 | if err != nil { 175 | return 176 | } 177 | case 2: // session params and electionID (single-primary) 178 | a.Logger.Printf("sending request=%v to %q", modParams[0], t.Config.Name) 179 | err = modClient.Send(modParams[0]) 180 | if err != nil { 181 | rspCh <- &modifyResponse{ 182 | TargetError: TargetError{ 183 | TargetName: t.Config.Name, 184 | Err: err, 185 | }, 186 | } 187 | return 188 | } 189 | modRsp, err := modClient.Recv() 190 | rspCh <- &modifyResponse{ 191 | TargetError: TargetError{ 192 | TargetName: t.Config.Name, 193 | Err: err, 194 | }, 195 | rsp: modRsp, 196 | } 197 | if err != nil { 198 | return 199 | } 200 | 201 | // send election ID 202 | a.Logger.Printf("sending request=%v to %q", modParams[1], t.Config.Name) 203 | err = modClient.Send(modParams[1]) 204 | if err != nil { 205 | rspCh <- &modifyResponse{ 206 | TargetError: TargetError{ 207 | TargetName: t.Config.Name, 208 | Err: err, 209 | }, 210 | } 211 | return 212 | } 213 | modRsp, err = modClient.Recv() 214 | rspCh <- &modifyResponse{ 215 | TargetError: TargetError{ 216 | TargetName: t.Config.Name, 217 | Err: err, 218 | }, 219 | rsp: modRsp, 220 | } 221 | if err != nil { 222 | return 223 | } 224 | if a.electionID != nil && modRsp.ElectionId != nil { 225 | if a.electionID.High < modRsp.ElectionId.High { 226 | a.Logger.Infof("target's last known electionID is higher than client's: %+v > %+v", modRsp.ElectionId, a.electionID) 227 | return 228 | } 229 | if a.electionID.High == modRsp.ElectionId.High && a.electionID.Low < modRsp.ElectionId.Low { 230 | a.Logger.Infof("target's last known electionID is higher than client's: %+v > %+v", modRsp.ElectionId, a.electionID) 231 | return 232 | } 233 | } 234 | } 235 | modReqs, err := a.createModifyRequestOperation(modifyInput) 236 | if err != nil { 237 | rspCh <- &modifyResponse{ 238 | TargetError: TargetError{ 239 | TargetName: t.Config.Name, 240 | Err: err, 241 | }, 242 | } 243 | return 244 | } 245 | // operations 246 | for _, req := range modReqs { 247 | a.Logger.Infof("target %s modify request:\n%s", t.Config.Name, prototext.Format(req)) 248 | err = modClient.Send(req) 249 | if err != nil { 250 | rspCh <- &modifyResponse{ 251 | TargetError: TargetError{ 252 | TargetName: t.Config.Name, 253 | Err: err, 254 | }, 255 | } 256 | return 257 | } 258 | modRsp, err := modClient.Recv() 259 | rspCh <- &modifyResponse{ 260 | TargetError: TargetError{ 261 | TargetName: t.Config.Name, 262 | Err: err, 263 | }, 264 | rsp: modRsp, 265 | } 266 | if err != nil { 267 | return 268 | } 269 | for _, result := range modRsp.GetResult() { 270 | switch result.GetStatus() { 271 | case spb.AFTResult_UNSET: // TODO: consider this an error ? 272 | // case spb.AFTResult_OK: DEPRECATED 273 | case spb.AFTResult_FAILED: 274 | return 275 | case spb.AFTResult_RIB_PROGRAMMED: 276 | case spb.AFTResult_FIB_PROGRAMMED: 277 | case spb.AFTResult_FIB_FAILED: 278 | return 279 | } 280 | } 281 | } 282 | }() 283 | 284 | return rspCh 285 | } 286 | 287 | func (a *App) createModifyRequestParams(modifyInput *config.ModifyInput) ([]*spb.ModifyRequest, error) { 288 | if modifyInput.Params == nil { 289 | modReq, err := api.NewModifyRequest( 290 | api.PersistenceDelete(), 291 | api.RedundancyAllPrimary(), 292 | api.AckTypeRib(), 293 | ) 294 | return []*spb.ModifyRequest{modReq}, err 295 | } 296 | 297 | opts := make([]api.GRIBIOption, 0, 4) 298 | 299 | switch { 300 | case a.Config.ModifySessionPersistancePreserve || 301 | (modifyInput.Params.Persistence == "preserve" && !a.Config.ModifySessionPersistancePreserve): 302 | opts = append(opts, api.PersistencePreserve()) 303 | default: 304 | opts = append(opts, api.PersistenceDelete()) 305 | } 306 | 307 | switch { 308 | case a.Config.ModifySessionRedundancySinglePrimary || 309 | (modifyInput.Params.Redundancy == "single-primary" && !a.Config.ModifySessionRedundancySinglePrimary): 310 | opts = append(opts, 311 | api.RedundancySinglePrimary(), 312 | // , 313 | ) 314 | default: 315 | opts = append(opts, api.RedundancyAllPrimary()) 316 | } 317 | 318 | switch { 319 | case a.Config.ModifySessionRibFibAck || 320 | (modifyInput.Params.AckType == "rib-fib" && !a.Config.ModifySessionRibFibAck): 321 | opts = append(opts, api.AckTypeRibFib()) 322 | default: 323 | opts = append(opts, api.AckTypeRib()) 324 | } 325 | sessParams, err := api.NewModifyRequest(opts...) 326 | if err != nil { 327 | return nil, err 328 | } 329 | elecIdReq, err := api.NewModifyRequest(api.ElectionID(a.electionID)) 330 | if err != nil { 331 | return nil, err 332 | } 333 | return []*spb.ModifyRequest{sessParams, elecIdReq}, err 334 | } 335 | 336 | func (a *App) createModifyRequestOperation(modifyInput *config.ModifyInput) ([]*spb.ModifyRequest, error) { 337 | reqs := make([]*spb.ModifyRequest, 0) 338 | 339 | for _, op := range modifyInput.Operations { 340 | req := new(spb.ModifyRequest) 341 | aftOp, err := op.CreateAftOper() 342 | if err != nil { 343 | return nil, err 344 | } 345 | req.Operation = append(req.Operation, aftOp) 346 | reqs = append(reqs, req) 347 | } 348 | return reqs, nil 349 | } 350 | 351 | func (a *App) modifyChan(ctx context.Context, t *target, modReqCh chan *spb.ModifyRequest) (chan *spb.ModifyResponse, chan error) { 352 | rspChan := make(chan *spb.ModifyResponse) 353 | errChan := make(chan error, 1) 354 | m := new(sync.Mutex) 355 | ops := make(map[uint64]struct{}) 356 | // stream sending goroutine 357 | go func() { 358 | var err error 359 | for { 360 | select { 361 | case <-ctx.Done(): 362 | return 363 | case req, ok := <-modReqCh: 364 | if !ok { 365 | return 366 | } 367 | m.Lock() 368 | for _, op := range req.GetOperation() { 369 | ops[op.GetId()] = struct{}{} 370 | } 371 | m.Unlock() 372 | err = t.modClient.Send(req) 373 | if err != nil { 374 | errChan <- fmt.Errorf("failed sending request: %v: err=%v", req, err) 375 | return 376 | } 377 | } 378 | } 379 | }() 380 | // receive stream 381 | go func() { 382 | defer close(rspChan) 383 | for { 384 | a.mrcv.Lock() 385 | modRsp, err := t.modClient.Recv() 386 | a.mrcv.Unlock() 387 | if err != nil { 388 | errChan <- err 389 | return 390 | } 391 | rspChan <- modRsp 392 | m.Lock() 393 | for _, res := range modRsp.GetResult() { 394 | delete(ops, res.GetId()) 395 | } 396 | if len(ops) == 0 { 397 | m.Unlock() 398 | return 399 | } 400 | m.Unlock() 401 | } 402 | }() 403 | 404 | return rspChan, errChan 405 | } 406 | -------------------------------------------------------------------------------- /app/workflow.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | spb "github.com/openconfig/gribi/v1/proto/service" 14 | "google.golang.org/protobuf/proto" 15 | 16 | "github.com/karimra/gribic/config" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/pflag" 19 | ) 20 | 21 | func (a *App) InitWorkflowFlags(cmd *cobra.Command) { 22 | cmd.ResetFlags() 23 | // 24 | cmd.Flags().StringVarP(&a.Config.WorkflowFile, "file", "", "", "workflow file") 25 | // 26 | cmd.Flags().VisitAll(func(flag *pflag.Flag) { 27 | a.Config.FileConfig.BindPFlag(fmt.Sprintf("%s-%s", cmd.Name(), flag.Name), flag) 28 | }) 29 | } 30 | 31 | func (a *App) WorkflowPreRunE(cmd *cobra.Command, args []string) error { 32 | if a.Config.WorkflowFile == "" { 33 | return errors.New("missing --file value") 34 | } 35 | 36 | err := a.Config.ReadWorkflowFile() 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (a *App) WorkflowRunE(cmd *cobra.Command, args []string) error { 44 | targets, err := a.GetTargets() 45 | if err != nil { 46 | return err 47 | } 48 | a.Logger.Debugf("targets: %v", targets) 49 | numTargets := len(targets) 50 | a.wg.Add(numTargets) 51 | errCh := make(chan error, numTargets) 52 | for _, t := range targets { 53 | go func(t *target) { 54 | defer a.wg.Done() 55 | // render the workflow 56 | wf, err := a.Config.GenerateWorkflow(t.Config.Name) 57 | if err != nil { 58 | errCh <- fmt.Errorf("target=%q: failed to generate workflow: %v", t.Config.Name, err) 59 | return 60 | } 61 | 62 | // create context 63 | ctx, cancel := context.WithCancel(a.ctx) 64 | defer cancel() 65 | // append credentials to context 66 | ctx = appendCredentials(ctx, t.Config) 67 | // create a gRPC conn 68 | err = a.CreateGrpcClient(ctx, t, a.createBaseDialOpts()...) 69 | if err != nil { 70 | errCh <- fmt.Errorf("target=%q: failed to create a GRPC client: %v", t.Config.Name, err) 71 | return 72 | } 73 | defer t.Close() 74 | // 75 | ex, err := a.runWorkflow(ctx, t, wf) 76 | if err != nil { 77 | a.Logger.Errorf("target=%q: failed run workflow: %v", t.Config.Name, err) 78 | errCh <- fmt.Errorf("target=%q: failed run workflow: %v", t.Config.Name, err) 79 | return 80 | } 81 | a.pm.Lock() 82 | fmt.Println(ex.String()) 83 | a.pm.Unlock() 84 | }(t) 85 | } 86 | a.wg.Wait() 87 | close(errCh) 88 | 89 | errs := make([]error, 0) //, numTargets) 90 | for err := range errCh { 91 | if err != nil { 92 | errs = append(errs, err) 93 | } 94 | } 95 | return a.handleErrs(errs) 96 | } 97 | 98 | func (a *App) runWorkflow(ctx context.Context, t *target, wf *config.Workflow) (*execution, error) { 99 | if wf == nil { 100 | return nil, errors.New("nil workflow") 101 | } 102 | if len(wf.Steps) == 0 { 103 | return nil, fmt.Errorf("workflow %q has no steps", wf.Name) 104 | } 105 | 106 | exec := newExec(wf) 107 | 108 | t.gRIBIClient = spb.NewGRIBIClient(t.conn) 109 | // run steps 110 | ctx, cancel := context.WithCancel(ctx) 111 | defer cancel() 112 | for i, s := range wf.Steps { 113 | if s.Name == "" { 114 | s.Name = fmt.Sprintf("%s.%d", wf.Name, i+1) 115 | } 116 | a.Logger.Infof("workflow=%q: target=%q: step=%s: start", wf.Name, t.Config.Name, s.Name) 117 | reqs, err := s.BuildRequests() 118 | if err != nil { 119 | exec.addStep(workflowStepExecution{ 120 | Timestamp: time.Now(), 121 | Workflow: wf.Name, 122 | Step: s.Name, 123 | Target: t.Config.Name, 124 | Error: err, 125 | }) 126 | return exec, err 127 | } 128 | a.Logger.Debugf("workflow=%q: target=%q: step=%s: requests: %+v", wf.Name, t.Config.Name, s.Name, reqs) 129 | // wait duration if any 130 | a.Logger.Infof("workflow=%q: target=%q: step=%s: waiting %s", wf.Name, t.Config.Name, s.Name, s.Wait) 131 | time.Sleep(s.Wait) 132 | switch rpc := strings.ToLower(s.RPC); rpc { 133 | case "get": 134 | OUTER: 135 | for _, req := range reqs { 136 | switch req := req.ProtoReflect().Interface().(type) { 137 | case *spb.GetRequest: 138 | a.Logger.Infof("workflow=%q: target=%q: step=%s: %T: %v", wf.Name, t.Config.Name, s.Name, req, req) 139 | exec.addStep(workflowStepExecution{ 140 | Timestamp: time.Now(), 141 | Workflow: wf.Name, 142 | Step: s.Name, 143 | Target: t.Config.Name, 144 | RPC: rpc, 145 | Request: req, 146 | }) 147 | rspCh, errCh := a.getChan(ctx, t, req) 148 | for { 149 | select { 150 | case <-ctx.Done(): 151 | exec.addStep(workflowStepExecution{ 152 | Timestamp: time.Now(), 153 | Workflow: wf.Name, 154 | Step: s.Name, 155 | Target: t.Config.Name, 156 | RPC: rpc, 157 | Error: ctx.Err(), 158 | }) 159 | return exec, ctx.Err() 160 | case rsp := <-rspCh: 161 | exec.addStep(workflowStepExecution{ 162 | Timestamp: time.Now(), 163 | Workflow: wf.Name, 164 | Step: s.Name, 165 | Target: t.Config.Name, 166 | RPC: rpc, 167 | Response: rsp, 168 | }) 169 | a.Logger.Infof("workflow=%q: target=%q: step=%s: %T: %v", wf.Name, t.Config.Name, s.Name, rsp, rsp) 170 | case err := <-errCh: 171 | if err == io.EOF { 172 | continue OUTER 173 | } 174 | exec.addStep(workflowStepExecution{ 175 | Timestamp: time.Now(), 176 | Workflow: wf.Name, 177 | Step: s.Name, 178 | Target: t.Config.Name, 179 | RPC: rpc, 180 | Error: err, 181 | }) 182 | return exec, err 183 | } 184 | } 185 | default: 186 | err = fmt.Errorf("workflow=%q: unexpected request type: expected GetRequest, got %T", wf.Name, req) 187 | exec.addStep(workflowStepExecution{ 188 | Timestamp: time.Now(), 189 | Workflow: wf.Name, 190 | Step: s.Name, 191 | Target: t.Config.Name, 192 | RPC: rpc, 193 | Error: err, 194 | }) 195 | return exec, err 196 | } 197 | } 198 | case "flush": 199 | for _, req := range reqs { 200 | switch req := req.ProtoReflect().Interface().(type) { 201 | case *spb.FlushRequest: 202 | a.Logger.Infof("workflow=%q: target=%q: step=%s: %T: %v", wf.Name, t.Config.Name, s.Name, req, req) 203 | exec.addStep(workflowStepExecution{ 204 | Timestamp: time.Now(), 205 | Workflow: wf.Name, 206 | Step: s.Name, 207 | Target: t.Config.Name, 208 | RPC: rpc, 209 | Request: req, 210 | }) 211 | rsp, err := a.flush(ctx, t, req) 212 | if err != nil { 213 | exec.addStep(workflowStepExecution{ 214 | Timestamp: time.Now(), 215 | Workflow: wf.Name, 216 | Step: s.Name, 217 | Target: t.Config.Name, 218 | RPC: rpc, 219 | Error: err, 220 | }) 221 | return exec, err 222 | } 223 | a.Logger.Infof("workflow=%q: target=%q: step=%s, %T: %v\n", wf.Name, t.Config.Name, s.Name, rsp, rsp) 224 | exec.addStep(workflowStepExecution{ 225 | Timestamp: time.Now(), 226 | Workflow: wf.Name, 227 | Step: s.Name, 228 | Target: t.Config.Name, 229 | RPC: rpc, 230 | Response: rsp, 231 | }) 232 | default: 233 | err = fmt.Errorf("workflow=%q: unexpected request type: expected FlushRequest, got %T", wf.Name, req) 234 | exec.addStep(workflowStepExecution{ 235 | Timestamp: time.Now(), 236 | Workflow: wf.Name, 237 | Step: s.Name, 238 | Target: t.Config.Name, 239 | RPC: rpc, 240 | Error: err, 241 | }) 242 | return exec, err 243 | } 244 | } 245 | case "modify": 246 | reqCh := make(chan *spb.ModifyRequest) 247 | if t.modClient == nil { 248 | t.modClient, err = t.gRIBIClient.Modify(ctx) 249 | if err != nil { 250 | err = fmt.Errorf("failed creating modify stream: %v", err) 251 | exec.addStep(workflowStepExecution{ 252 | Timestamp: time.Now(), 253 | Workflow: wf.Name, 254 | Step: s.Name, 255 | Target: t.Config.Name, 256 | RPC: rpc, 257 | Error: err, 258 | }) 259 | return exec, err 260 | } 261 | } 262 | doneCh := make(chan struct{}) 263 | go func() { 264 | rspCh, errCh := a.modifyChan(ctx, t, reqCh) 265 | for { 266 | select { 267 | case <-ctx.Done(): 268 | if ctx.Err() == nil || ctx.Err() == context.Canceled { 269 | return 270 | } 271 | a.Logger.Infof("workflow=%q: target=%q: step=%s: context done=%v", wf.Name, t.Config.Name, s.Name, ctx.Err()) 272 | exec.addStep(workflowStepExecution{ 273 | Timestamp: time.Now(), 274 | Workflow: wf.Name, 275 | Step: s.Name, 276 | Target: t.Config.Name, 277 | RPC: rpc, 278 | Error: ctx.Err(), 279 | }) 280 | return 281 | case rsp, ok := <-rspCh: 282 | if !ok { 283 | close(doneCh) 284 | return 285 | } 286 | a.Logger.Infof("workflow=%q: target=%q: step=%s: %T: %v", wf.Name, t.Config.Name, s.Name, rsp, rsp) 287 | exec.addStep(workflowStepExecution{ 288 | Timestamp: time.Now(), 289 | Workflow: wf.Name, 290 | Step: s.Name, 291 | Target: t.Config.Name, 292 | RPC: rpc, 293 | Response: rsp, 294 | }) 295 | case err, ok := <-errCh: 296 | if !ok { 297 | close(doneCh) 298 | return 299 | } 300 | a.Logger.Infof("workflow=%q: target=%q: step=%s: err=%v", t.Config.Name, wf.Name, s.Name, err) 301 | exec.addStep(workflowStepExecution{ 302 | Timestamp: time.Now(), 303 | Workflow: wf.Name, 304 | Step: s.Name, 305 | Target: t.Config.Name, 306 | RPC: rpc, 307 | Error: err, 308 | }) 309 | return 310 | } 311 | } 312 | }() 313 | for _, req := range reqs { 314 | switch req := req.ProtoReflect().Interface().(type) { 315 | case *spb.ModifyRequest: 316 | a.Logger.Infof("workflow=%q: target=%q: step=%s: %T: %v", wf.Name, t.Config.Name, s.Name, req, req) 317 | exec.addStep(workflowStepExecution{ 318 | Timestamp: time.Now(), 319 | Workflow: wf.Name, 320 | Step: s.Name, 321 | Target: t.Config.Name, 322 | RPC: rpc, 323 | Request: req, 324 | }) 325 | reqCh <- req 326 | default: 327 | err = fmt.Errorf("workflow=%q: unexpected request type: expected ModifyRequest, got %T", wf.Name, req) 328 | exec.addStep(workflowStepExecution{ 329 | Timestamp: time.Now(), 330 | Workflow: wf.Name, 331 | Step: s.Name, 332 | Target: t.Config.Name, 333 | RPC: rpc, 334 | Error: err, 335 | }) 336 | return exec, err 337 | } 338 | } 339 | <-doneCh 340 | } 341 | 342 | // wait duration if any 343 | a.Logger.Infof("workflow=%q: target=%q: step=%s: waiting %s after execution", wf.Name, t.Config.Name, s.Name, s.WaitAfter) 344 | time.Sleep(s.WaitAfter) 345 | } 346 | return exec, nil 347 | } 348 | 349 | type workflowStepExecution struct { 350 | Timestamp time.Time `json:"timestamp,omitempty"` 351 | Workflow string `json:"workflow,omitempty"` 352 | Step string `json:"step,omitempty"` 353 | Target string `json:"target,omitempty"` 354 | RPC string `json:"rpc,omitempty"` 355 | Request proto.Message `json:"request,omitempty"` 356 | Response proto.Message `json:"response,omitempty"` 357 | Error error `json:"error,omitempty"` 358 | } 359 | 360 | type execution struct { 361 | wf *config.Workflow 362 | m *sync.Mutex 363 | result []workflowStepExecution 364 | } 365 | 366 | func newExec(wf *config.Workflow) *execution { 367 | return &execution{ 368 | wf: wf, 369 | m: &sync.Mutex{}, 370 | result: []workflowStepExecution{}, 371 | } 372 | } 373 | 374 | func (e *execution) addStep(wse workflowStepExecution) { 375 | e.m.Lock() 376 | defer e.m.Unlock() 377 | e.result = append(e.result, wse) 378 | } 379 | 380 | func (e *execution) String() string { 381 | b, _ := json.MarshalIndent(e.result, "", " ") 382 | return string(b) 383 | } 384 | -------------------------------------------------------------------------------- /config/modify.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/karimra/gnmic/utils" 16 | "github.com/karimra/gribic/api" 17 | spb "github.com/openconfig/gribi/v1/proto/service" 18 | "gopkg.in/yaml.v2" 19 | ) 20 | 21 | const ( 22 | varFileSuffix = "_vars" 23 | ) 24 | 25 | type OperationConfig struct { 26 | ID uint64 `yaml:"id,omitempty" json:"id,omitempty"` 27 | NetworkInstance string `yaml:"network-instance,omitempty" json:"network-instance,omitempty"` 28 | Operation string `yaml:"op,omitempty" json:"operation,omitempty"` 29 | // 30 | IPv6 *ipv4v6Entry `yaml:"ipv6,omitempty" json:"ipv6,omitempty"` 31 | IPv4 *ipv4v6Entry `yaml:"ipv4,omitempty" json:"ipv4,omitempty"` 32 | NHG *nhgEntry `yaml:"nhg,omitempty" json:"nhg,omitempty"` 33 | NH *nhEntry `yaml:"nh,omitempty" json:"nh,omitempty"` 34 | // 35 | ElectionID string `yaml:"election-id,omitempty" json:"election-id,omitempty"` 36 | // 37 | electionID *spb.Uint128 38 | } 39 | 40 | func (oc *OperationConfig) String() string { 41 | b, _ := json.MarshalIndent(oc, "", " ") 42 | return string(b) 43 | } 44 | 45 | func (oc *OperationConfig) validate() error { 46 | if oc.IPv4 == nil && oc.IPv6 == nil && oc.NHG == nil && oc.NH == nil { 47 | return errors.New("missing entry") 48 | } 49 | if oc.IPv4 != nil { 50 | if oc.IPv6 != nil { 51 | return errors.New("both ipv4 and ipv6 entries are defined") 52 | } 53 | if oc.NHG != nil { 54 | return errors.New("both ipv4 and nhg entries are defined") 55 | } 56 | if oc.NH != nil { 57 | return errors.New("both ipv4 and nh entries are defined") 58 | } 59 | return nil 60 | } 61 | if oc.IPv6 != nil { 62 | if oc.NHG != nil { 63 | return errors.New("both ipv6 and nhg entries are defined") 64 | } 65 | if oc.NH != nil { 66 | return errors.New("both ipv6 and nh entries are defined") 67 | } 68 | return nil 69 | } 70 | if oc.NHG != nil { 71 | if oc.NH != nil { 72 | return errors.New("both nhg and nh entries are defined") 73 | } 74 | return nil 75 | } 76 | return nil 77 | } 78 | 79 | func (o *OperationConfig) calculateElectionID() error { 80 | if o.ElectionID == "" { 81 | return nil 82 | } 83 | var err error 84 | o.electionID, err = ParseUint128(o.ElectionID) 85 | return err 86 | } 87 | 88 | func (o *OperationConfig) CreateAftOper() (*spb.AFTOperation, error) { 89 | err := o.calculateElectionID() 90 | if err != nil { 91 | return nil, err 92 | } 93 | opts := []api.GRIBIOption{ 94 | api.ID(o.ID), 95 | api.NetworkInstance(o.NetworkInstance), 96 | api.Op(o.Operation), 97 | api.ElectionID(o.electionID), 98 | } 99 | 100 | switch { 101 | case o.IPv6 != nil: 102 | // append IPv6Entry option 103 | opts = append(opts, 104 | api.IPv6Entry( 105 | api.Prefix(o.IPv6.Prefix), 106 | api.DecapsulateHeader(o.IPv6.DecapsulateHeader), 107 | api.Metadata([]byte(o.IPv6.EntryMetadata)), 108 | api.NHG(o.IPv6.NHG), 109 | api.NetworkInstance(o.IPv6.NHGNetworkInstance), 110 | ), 111 | ) 112 | case o.IPv4 != nil: 113 | // append IPv4Entry option 114 | opts = append(opts, 115 | api.IPv4Entry( 116 | api.Prefix(o.IPv4.Prefix), 117 | api.DecapsulateHeader(o.IPv4.DecapsulateHeader), 118 | api.Metadata([]byte(o.IPv4.EntryMetadata)), 119 | api.NHG(o.IPv4.NHG), 120 | api.NetworkInstance(o.IPv4.NHGNetworkInstance), 121 | ), 122 | ) 123 | case o.NH != nil: 124 | nheOpts := []api.GRIBIOption{ 125 | api.Index(o.NH.Index), 126 | api.EncapsulateHeader(o.NH.EncapsulateHeader), 127 | api.DecapsulateHeader(o.NH.DecapsulateHeader), 128 | api.IPAddress(o.NH.IPAddress), 129 | api.MAC(o.NH.MAC), 130 | api.NetworkInstance(o.NH.NetworkInstance), 131 | } 132 | if o.NH.InterfaceReference != nil { 133 | if o.NH.InterfaceReference.Interface != "" { 134 | nheOpts = append(nheOpts, api.Interface(o.NH.InterfaceReference.Interface)) 135 | } 136 | if o.NH.InterfaceReference.Subinterface != nil { 137 | nheOpts = append(nheOpts, 138 | api.SubInterface(*o.NH.InterfaceReference.Subinterface), 139 | ) 140 | } 141 | } 142 | if o.NH.IPinIP != nil { 143 | nheOpts = append(nheOpts, 144 | api.IPinIP(o.NH.IPinIP.SRCIP, o.NH.IPinIP.DSTIP), 145 | ) 146 | } 147 | 148 | for _, pmls := range o.NH.PushedMPLSLabelStack { 149 | nheOpts = append(nheOpts, 150 | api.PushedMplsLabelStack(pmls.Type, uint64(pmls.Label)), 151 | ) 152 | } 153 | 154 | // create NH Entry Option 155 | opts = append(opts, api.NHEntry(nheOpts...)) 156 | case o.NHG != nil: 157 | nhgeOpts := []api.GRIBIOption{ 158 | api.ID(o.NHG.ID), 159 | } 160 | if o.NHG.BackupNHG != nil { 161 | nhgeOpts = append(nhgeOpts, api.BackupNextHopGroup(*o.NHG.BackupNHG)) 162 | } 163 | if o.NHG.Color != nil { 164 | nhgeOpts = append(nhgeOpts, api.Color(*o.NHG.Color)) 165 | } 166 | 167 | for _, nh := range o.NHG.NextHop { 168 | nhgeOpts = append(nhgeOpts, api.NHGNextHop(nh.Index, nh.Weight)) 169 | } 170 | // create NHG Entry Option 171 | opts = append(opts, api.NHGEntry(nhgeOpts...)) 172 | } 173 | return api.NewAFTOperation(opts...) 174 | } 175 | 176 | type ModifyInput struct { 177 | DefaultNetworkInstance string `yaml:"default-network-instance" json:"default-network-instance,omitempty"` 178 | DefaultOperation string `yaml:"default-operation" json:"default-operation,omitempty"` 179 | Params *sessionParams `yaml:"params,omitempty" json:"params,omitempty"` 180 | Operations []*OperationConfig `yaml:"operations,omitempty" json:"operations,omitempty"` 181 | } 182 | 183 | type sessionParams struct { 184 | Redundancy string `yaml:"redundancy,omitempty" json:"redundancy,omitempty"` 185 | Persistence string `yaml:"persistence,omitempty" json:"persistence,omitempty"` 186 | AckType string `yaml:"ack-type,omitempty" json:"ack-type,omitempty"` 187 | } 188 | 189 | type ipv4v6Entry struct { 190 | Type string `yaml:"type,omitempty" json:"type,omitempty"` 191 | // ipv4v6 192 | Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` 193 | NHG uint64 `yaml:"nhg,omitempty" json:"nhg,omitempty"` 194 | NHGNetworkInstance string `yaml:"nhg-network-instance,omitempty" json:"nhg-network-instance,omitempty"` 195 | DecapsulateHeader string `yaml:"decapsulate-header,omitempty" json:"decapsulate-header,omitempty"` 196 | EntryMetadata string `yaml:"entry-metadata,omitempty" json:"entry-metadata,omitempty"` 197 | } 198 | 199 | type nhgEntry struct { 200 | Type string `yaml:"type,omitempty" json:"type,omitempty"` 201 | // nhg 202 | ID uint64 `yaml:"id,omitempty" json:"id,omitempty"` 203 | BackupNHG *uint64 `yaml:"backup-nhg,omitempty" json:"backup-nhg,omitempty"` 204 | Color *uint64 `yaml:"color,omitempty" json:"color,omitempty"` 205 | NextHop []struct { 206 | Index uint64 `yaml:"index,omitempty" json:"index,omitempty"` 207 | Weight uint64 `yaml:"weight,omitempty" json:"weight,omitempty"` 208 | } `yaml:"next-hop,omitempty" json:"next-hop,omitempty"` 209 | ProgrammedID *uint64 `yaml:"programmed-id,omitempty" json:"programmed-id,omitempty"` 210 | } 211 | 212 | type nhEntry struct { 213 | Type string `yaml:"type,omitempty" json:"type,omitempty"` 214 | // nh 215 | Index uint64 `yaml:"index,omitempty" json:"index,omitempty"` 216 | DecapsulateHeader string `yaml:"decapsulate-header,omitempty" json:"decapsulate-header,omitempty"` 217 | EncapsulateHeader string `yaml:"encapsulate-header,omitempty" json:"encapsulate-header,omitempty"` 218 | IPAddress string `yaml:"ip-address,omitempty" json:"ip-address,omitempty"` 219 | InterfaceReference *interfaceReference `yaml:"interface-reference,omitempty" json:"interface-reference,omitempty"` 220 | IPinIP *ipinip `yaml:"ip-in-ip,omitempty" json:"ip-in-ip,omitempty"` 221 | MAC string `yaml:"mac,omitempty" json:"mac,omitempty"` 222 | NetworkInstance string `yaml:"network-instance,omitempty" json:"network-instance,omitempty"` 223 | ProgrammedIndex *uint64 `yaml:"programmed-index,omitempty" json:"programmed-index,omitempty"` 224 | PushedMPLSLabelStack []struct { 225 | Type string `yaml:"type,omitempty" json:"type,omitempty"` 226 | Label uint `yaml:"label,omitempty" json:"label,omitempty"` 227 | } `yaml:"pushed-mpls-label-stack,omitempty" json:"pushed-mpls-label-stack,omitempty"` 228 | } 229 | 230 | type interfaceReference struct { 231 | Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` 232 | Subinterface *uint64 `yaml:"subinterface,omitempty" json:"subinterface,omitempty"` 233 | } 234 | 235 | type ipinip struct { 236 | DSTIP string `yaml:"dst-ip,omitempty" json:"dst-ip,omitempty"` 237 | SRCIP string `yaml:"src-ip,omitempty" json:"src-ip,omitempty"` 238 | } 239 | 240 | func (c *Config) GenerateModifyInputs(targetName string) (*ModifyInput, error) { 241 | buf := new(bytes.Buffer) 242 | err := c.modifyInputTemplate.Execute(buf, templateInput{ 243 | TargetName: targetName, 244 | Vars: c.modifyInputVars, 245 | }) 246 | if err != nil { 247 | return nil, err 248 | } 249 | result := new(ModifyInput) 250 | err = yaml.Unmarshal(buf.Bytes(), result) 251 | if err != nil { 252 | return nil, err 253 | } 254 | sortOperations(result.Operations, "DRA") 255 | for i, op := range result.Operations { 256 | if op.NetworkInstance == "" { 257 | op.NetworkInstance = result.DefaultNetworkInstance 258 | } 259 | if op.Operation == "" { 260 | op.Operation = result.DefaultOperation 261 | } 262 | op.ID = uint64(i) + 1 263 | err = op.validate() 264 | if err != nil { 265 | return nil, fmt.Errorf("operation index %d is invalid: %w", op.ID, err) 266 | } 267 | } 268 | return result, err 269 | } 270 | 271 | func (c *Config) ReadModifyFileTemplate() error { 272 | b, err := os.ReadFile(c.ModifyInputFile) 273 | if err != nil { 274 | return err 275 | } 276 | c.modifyInputTemplate, err = utils.CreateTemplate("modify-rpc-input", string(b)) 277 | if err != nil { 278 | return err 279 | } 280 | return c.readTemplateVarsFile() 281 | } 282 | 283 | func (c *Config) readTemplateVarsFile() error { 284 | if c.ModifyInputVarsFile == "" { 285 | ext := filepath.Ext(c.ModifyInputFile) 286 | c.ModifyInputVarsFile = fmt.Sprintf("%s%s%s", c.ModifyInputFile[0:len(c.ModifyInputFile)-len(ext)], varFileSuffix, ext) 287 | c.logger.Printf("trying to find variable file %q", c.ModifyInputVarsFile) 288 | _, err := os.Stat(c.ModifyInputVarsFile) 289 | if os.IsNotExist(err) { 290 | c.ModifyInputVarsFile = "" 291 | return nil 292 | } else if err != nil { 293 | return err 294 | } 295 | } 296 | b, err := readFile(c.ModifyInputVarsFile) 297 | if err != nil { 298 | return err 299 | } 300 | if c.modifyInputVars == nil { 301 | c.modifyInputVars = make(map[string]interface{}) 302 | } 303 | err = yaml.Unmarshal(b, &c.modifyInputVars) 304 | if err != nil { 305 | return err 306 | } 307 | tempInterface := utils.Convert(c.modifyInputVars) 308 | switch t := tempInterface.(type) { 309 | case map[string]interface{}: 310 | c.modifyInputVars = t 311 | default: 312 | return errors.New("unexpected variables file format") 313 | } 314 | if c.Debug { 315 | c.logger.Printf("request vars content: %v", c.modifyInputVars) 316 | } 317 | return nil 318 | } 319 | 320 | func ParseUint128(v string) (*spb.Uint128, error) { 321 | if v == "" { 322 | return nil, nil 323 | } 324 | if strings.HasPrefix(v, ":") { 325 | v = "0" + v 326 | } 327 | if strings.HasSuffix(v, ":") { 328 | v = v + "0" 329 | } 330 | 331 | lh := strings.SplitN(v, ":", 2) 332 | switch len(lh) { 333 | case 1: 334 | vi, err := strconv.Atoi(lh[0]) 335 | if err != nil { 336 | return nil, err 337 | } 338 | return &spb.Uint128{Low: uint64(vi)}, nil 339 | case 2: 340 | if lh[0] == "" { 341 | lh[0] = "0" 342 | } 343 | v0i, err := strconv.Atoi(lh[0]) 344 | if err != nil { 345 | return nil, err 346 | } 347 | if lh[1] == "" { 348 | lh[1] = "0" 349 | } 350 | v1i, err := strconv.Atoi(lh[1]) 351 | if err != nil { 352 | return nil, err 353 | } 354 | return &spb.Uint128{High: uint64(v0i), Low: uint64(v1i)}, nil 355 | } 356 | return nil, nil 357 | } 358 | 359 | // readFile reads a json or yaml file. the the file is .yaml, converts it to json and returns []byte and an error 360 | func readFile(name string) ([]byte, error) { 361 | data, err := utils.ReadFile(context.TODO(), name) 362 | if err != nil { 363 | return nil, err 364 | } 365 | // 366 | switch filepath.Ext(name) { 367 | case ".json": 368 | return data, err 369 | case ".yaml", ".yml": 370 | return tryYAML(data) 371 | default: 372 | // try yaml 373 | newData, err := tryYAML(data) 374 | if err != nil { 375 | // assume json 376 | return data, nil 377 | } 378 | return newData, nil 379 | } 380 | } 381 | 382 | func tryYAML(data []byte) ([]byte, error) { 383 | var out interface{} 384 | var err error 385 | err = yaml.Unmarshal(data, &out) 386 | if err != nil { 387 | return nil, err 388 | } 389 | newStruct := utils.Convert(out) 390 | newData, err := json.Marshal(newStruct) 391 | if err != nil { 392 | return nil, err 393 | } 394 | return newData, nil 395 | } 396 | 397 | type templateInput struct { 398 | TargetName string 399 | Vars map[string]interface{} 400 | } 401 | 402 | // sortOperations sorts the given []*OperationConfig slice based on the ordering type 403 | // string. For example DRA = DELETE, REPLACE, ADD 404 | func sortOperations(ops []*OperationConfig, order string) { 405 | switch strings.ToUpper(order) { 406 | case "DRA": 407 | sortOperationsDRA(ops) 408 | case "DAR": 409 | sortOperationsDAR(ops) 410 | case "ARD": 411 | sortOperationsARD(ops) 412 | case "ADR": 413 | sortOperationsADR(ops) 414 | case "RAD": 415 | sortOperationsRAD(ops) 416 | case "RDA": 417 | sortOperationsRDA(ops) 418 | } 419 | } 420 | 421 | // sortOperationsDRA sorts the given []*OperationConfig by operation type then by entry type. 422 | // Operation type sort order is: Deletes, Replaces then Additions. 423 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 424 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 425 | func sortOperationsDRA(ops []*OperationConfig) { 426 | sort.SliceStable(ops, func(i, j int) bool { 427 | switch strings.ToUpper(ops[i].Operation) { 428 | case "DELETE": 429 | switch strings.ToUpper(ops[j].Operation) { 430 | case "DELETE": 431 | return lessDeleteOp(ops[i], ops[j]) 432 | case "ADD": 433 | return true 434 | case "REPLACE": 435 | return true 436 | } 437 | case "REPLACE": 438 | switch strings.ToUpper(ops[j].Operation) { 439 | case "DELETE": 440 | return false 441 | case "ADD": 442 | return true 443 | case "REPLACE": 444 | return lessAddOrReplaceOp(ops[i], ops[j]) 445 | } 446 | case "ADD": 447 | switch strings.ToUpper(ops[j].Operation) { 448 | case "DELETE": 449 | return false 450 | case "REPLACE": 451 | return false 452 | case "ADD": 453 | return lessAddOrReplaceOp(ops[i], ops[j]) 454 | } 455 | } 456 | return false 457 | }) 458 | } 459 | 460 | // sortOperationsDAR sorts the given []*OperationConfig by operation type then by entry type. 461 | // Operation type sort order is: Deletes, Additions then Replaces. 462 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 463 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 464 | func sortOperationsDAR(ops []*OperationConfig) { 465 | sort.Slice(ops, func(i, j int) bool { 466 | switch strings.ToUpper(ops[i].Operation) { 467 | case "DELETE": 468 | switch strings.ToUpper(ops[j].Operation) { 469 | case "DELETE": 470 | return lessDeleteOp(ops[i], ops[j]) 471 | case "ADD": 472 | return true 473 | case "REPLACE": 474 | return true 475 | } 476 | case "REPLACE": 477 | switch strings.ToUpper(ops[j].Operation) { 478 | case "DELETE": 479 | return false 480 | case "ADD": 481 | return true 482 | case "REPLACE": 483 | return lessAddOrReplaceOp(ops[i], ops[j]) 484 | } 485 | case "ADD": 486 | switch strings.ToUpper(ops[j].Operation) { 487 | case "DELETE": 488 | return false 489 | case "REPLACE": 490 | return true 491 | case "ADD": 492 | return lessAddOrReplaceOp(ops[i], ops[j]) 493 | } 494 | } 495 | return false 496 | }) 497 | } 498 | 499 | // sortOperationsARD sorts the given []*OperationConfig by operation type then by entry type. 500 | // Operation type sort order is: Additions, Replaces then Deletes. 501 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 502 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 503 | func sortOperationsARD(ops []*OperationConfig) { 504 | sort.Slice(ops, func(i, j int) bool { 505 | switch strings.ToUpper(ops[i].Operation) { 506 | case "DELETE": 507 | switch strings.ToUpper(ops[j].Operation) { 508 | case "DELETE": 509 | return lessDeleteOp(ops[i], ops[j]) 510 | case "ADD": 511 | return true 512 | case "REPLACE": 513 | return true 514 | } 515 | case "REPLACE": 516 | switch strings.ToUpper(ops[j].Operation) { 517 | case "DELETE": 518 | return true 519 | case "ADD": 520 | return false 521 | case "REPLACE": 522 | return lessAddOrReplaceOp(ops[i], ops[j]) 523 | } 524 | case "ADD": 525 | switch strings.ToUpper(ops[j].Operation) { 526 | case "DELETE": 527 | return true 528 | case "REPLACE": 529 | return true 530 | case "ADD": 531 | return lessAddOrReplaceOp(ops[i], ops[j]) 532 | } 533 | } 534 | return false 535 | }) 536 | } 537 | 538 | // sortOperationsADR sorts the given []*OperationConfig by operation type then by entry type. 539 | // Operation type sort order is: Additions, Deletes then Replaces. 540 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 541 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 542 | func sortOperationsADR(ops []*OperationConfig) { 543 | sort.Slice(ops, func(i, j int) bool { 544 | switch strings.ToUpper(ops[i].Operation) { 545 | case "DELETE": 546 | switch strings.ToUpper(ops[j].Operation) { 547 | case "DELETE": 548 | return lessDeleteOp(ops[i], ops[j]) 549 | case "ADD": 550 | return false 551 | case "REPLACE": 552 | return true 553 | } 554 | case "REPLACE": 555 | switch strings.ToUpper(ops[j].Operation) { 556 | case "DELETE": 557 | return false 558 | case "ADD": 559 | return false 560 | case "REPLACE": 561 | return lessAddOrReplaceOp(ops[i], ops[j]) 562 | } 563 | case "ADD": 564 | switch strings.ToUpper(ops[j].Operation) { 565 | case "DELETE": 566 | return true 567 | case "REPLACE": 568 | return true 569 | case "ADD": 570 | return lessAddOrReplaceOp(ops[i], ops[j]) 571 | } 572 | } 573 | return false 574 | }) 575 | } 576 | 577 | // sortOperationsRAD sorts the given []*OperationConfig by operation type then by entry type. 578 | // Operation type sort order is: Replaces, Additions then Deletes. 579 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 580 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 581 | func sortOperationsRAD(ops []*OperationConfig) { 582 | sort.Slice(ops, func(i, j int) bool { 583 | switch strings.ToUpper(ops[i].Operation) { 584 | case "DELETE": 585 | switch strings.ToUpper(ops[j].Operation) { 586 | case "DELETE": 587 | return lessDeleteOp(ops[i], ops[j]) 588 | case "ADD": 589 | return false 590 | case "REPLACE": 591 | return false 592 | } 593 | case "REPLACE": 594 | switch strings.ToUpper(ops[j].Operation) { 595 | case "DELETE": 596 | return true 597 | case "ADD": 598 | return true 599 | case "REPLACE": 600 | return lessAddOrReplaceOp(ops[i], ops[j]) 601 | } 602 | case "ADD": 603 | switch strings.ToUpper(ops[j].Operation) { 604 | case "DELETE": 605 | return true 606 | case "REPLACE": 607 | return false 608 | case "ADD": 609 | return lessAddOrReplaceOp(ops[i], ops[j]) 610 | } 611 | } 612 | return false 613 | }) 614 | } 615 | 616 | // sortOperationsRDA sorts the given []*OperationConfig by operation type then by entry type. 617 | // Operation type sort order is: Replaces, Deletes then Additions. 618 | // within Deletes: IPv4/v6 entries are sent first then NHGs and finally NHs. 619 | // within Replaces or Additions: NH are sent first, then NHGs and last are IPv4/v6 entries 620 | func sortOperationsRDA(ops []*OperationConfig) { 621 | sort.Slice(ops, func(i, j int) bool { 622 | switch strings.ToUpper(ops[i].Operation) { 623 | case "DELETE": 624 | switch strings.ToUpper(ops[j].Operation) { 625 | case "DELETE": 626 | return lessDeleteOp(ops[i], ops[j]) 627 | case "ADD": 628 | return true 629 | case "REPLACE": 630 | return false 631 | } 632 | case "REPLACE": 633 | switch strings.ToUpper(ops[j].Operation) { 634 | case "DELETE": 635 | return true 636 | case "ADD": 637 | return true 638 | case "REPLACE": 639 | return lessAddOrReplaceOp(ops[i], ops[j]) 640 | } 641 | case "ADD": 642 | switch strings.ToUpper(ops[j].Operation) { 643 | case "DELETE": 644 | return false 645 | case "REPLACE": 646 | return false 647 | case "ADD": 648 | return lessAddOrReplaceOp(ops[i], ops[j]) 649 | } 650 | } 651 | return false 652 | }) 653 | } 654 | 655 | func lessAddOrReplaceOp(op1, op2 *OperationConfig) bool { 656 | switch { 657 | case op1.NH != nil: 658 | return true 659 | case op1.NHG != nil: 660 | switch { 661 | case op2.NH != nil: 662 | return false 663 | default: 664 | return true 665 | } 666 | case op1.IPv4 != nil: 667 | switch { 668 | case op2.NH != nil, op2.NHG != nil: 669 | return false 670 | default: 671 | return true 672 | } 673 | case op1.IPv6 != nil: 674 | switch { 675 | case op2.NH != nil, op2.NHG != nil: 676 | return false 677 | case op2.IPv4 != nil: 678 | return false 679 | default: 680 | return true 681 | } 682 | default: 683 | return true 684 | } 685 | } 686 | 687 | func lessDeleteOp(op1, op2 *OperationConfig) bool { 688 | switch { 689 | case op1.IPv6 != nil, op1.IPv4 != nil: 690 | switch { 691 | case op2.NH != nil, op2.NHG != nil: 692 | return true 693 | default: // ipv4/v6 694 | return false 695 | } 696 | case op1.NHG != nil: 697 | switch { 698 | case op2.IPv4 != nil, op2.IPv6 != nil: 699 | return false 700 | case op2.NH != nil: 701 | return true 702 | default: // nhg 703 | return true 704 | } 705 | case op1.NH != nil: 706 | switch { 707 | case op2.NH != nil: 708 | return true 709 | default: 710 | return false 711 | } 712 | default: 713 | return true 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /api/entry.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | gribi_aft "github.com/openconfig/gribi/v1/proto/gribi_aft" 8 | "github.com/openconfig/gribi/v1/proto/gribi_aft/enums" 9 | spb "github.com/openconfig/gribi/v1/proto/service" 10 | "github.com/openconfig/ygot/proto/ywrapper" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | func NewAFTOperation(opts ...GRIBIOption) (*spb.AFTOperation, error) { 15 | m := new(spb.AFTOperation) 16 | err := apply(m, opts...) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return m, nil 21 | } 22 | 23 | // AFTOperation ID or NextHopGroup ID 24 | func ID(id uint64) func(proto.Message) error { 25 | return func(msg proto.Message) error { 26 | if msg == nil { 27 | return ErrInvalidMsgType 28 | } 29 | switch msg := msg.ProtoReflect().Interface().(type) { 30 | case *spb.AFTOperation: 31 | msg.Id = id 32 | case *gribi_aft.Afts_NextHopGroupKey: 33 | msg.Id = id 34 | default: 35 | return fmt.Errorf("option ID: %w: %T", ErrInvalidMsgType, msg) 36 | } 37 | return nil 38 | } 39 | } 40 | 41 | // AFTOperation Network Instance, or 42 | // NextHop Entry Network Instance, or 43 | // IPv4 Entry Network Instance. 44 | func NetworkInstance(ns string) func(proto.Message) error { 45 | return func(msg proto.Message) error { 46 | if msg == nil { 47 | return ErrInvalidMsgType 48 | } 49 | if ns == "" { 50 | return nil 51 | } 52 | switch msg := msg.ProtoReflect().Interface().(type) { 53 | case *spb.GetRequest: 54 | msg.NetworkInstance = &spb.GetRequest_Name{Name: ns} 55 | case *spb.FlushRequest: 56 | msg.NetworkInstance = &spb.FlushRequest_Name{Name: ns} 57 | case *spb.AFTOperation: 58 | msg.NetworkInstance = ns 59 | case *gribi_aft.Afts_NextHopKey: 60 | if msg.NextHop == nil { 61 | msg.NextHop = new(gribi_aft.Afts_NextHop) 62 | } 63 | msg.NextHop.NetworkInstance = &ywrapper.StringValue{Value: ns} 64 | case *gribi_aft.Afts_Ipv4EntryKey: 65 | if msg.Ipv4Entry == nil { 66 | msg.Ipv4Entry = new(gribi_aft.Afts_Ipv4Entry) 67 | } 68 | msg.Ipv4Entry.NextHopGroupNetworkInstance = &ywrapper.StringValue{Value: ns} 69 | case *gribi_aft.Afts_Ipv6EntryKey: 70 | if msg.Ipv6Entry == nil { 71 | msg.Ipv6Entry = new(gribi_aft.Afts_Ipv6Entry) 72 | } 73 | msg.Ipv6Entry.NextHopGroupNetworkInstance = &ywrapper.StringValue{Value: ns} 74 | default: 75 | return fmt.Errorf("option NetworkInstance: %w: %T", ErrInvalidMsgType, msg) 76 | } 77 | return nil 78 | } 79 | } 80 | 81 | // AFTOperation Network Instance 82 | func Op(op string) func(proto.Message) error { 83 | return func(msg proto.Message) error { 84 | if msg == nil { 85 | return ErrInvalidMsgType 86 | } 87 | switch msg := msg.ProtoReflect().Interface().(type) { 88 | case *spb.AFTOperation: 89 | switch strings.ToUpper(op) { 90 | case "ADD": 91 | msg.Op = spb.AFTOperation_ADD 92 | case "REPLACE": 93 | msg.Op = spb.AFTOperation_REPLACE 94 | case "DELETE": 95 | msg.Op = spb.AFTOperation_DELETE 96 | default: 97 | return fmt.Errorf("option Op: %w: %T", ErrInvalidValue, msg) 98 | } 99 | default: 100 | return fmt.Errorf("option Op: %w: %T", ErrInvalidMsgType, msg) 101 | } 102 | return nil 103 | } 104 | } 105 | 106 | func OpAdd() func(proto.Message) error { 107 | return Op("ADD") 108 | } 109 | 110 | func OpReplace() func(proto.Message) error { 111 | return Op("REPLACE") 112 | } 113 | 114 | func OpDelete() func(proto.Message) error { 115 | return Op("DELETE") 116 | } 117 | 118 | func ElectionID(id *spb.Uint128) func(proto.Message) error { 119 | return func(msg proto.Message) error { 120 | if msg == nil { 121 | return ErrInvalidMsgType 122 | } 123 | if id == nil { 124 | return nil 125 | } 126 | switch msg := msg.ProtoReflect().Interface().(type) { 127 | case *spb.AFTOperation: 128 | msg.ElectionId = id 129 | case *spb.FlushRequest: 130 | msg.Election = &spb.FlushRequest_Id{Id: id} 131 | case *spb.ModifyRequest: 132 | msg.ElectionId = id 133 | default: 134 | return fmt.Errorf("option ElectionID: %w: %T", ErrInvalidMsgType, msg) 135 | } 136 | return nil 137 | } 138 | } 139 | 140 | // Next Hop Options 141 | func NHEntry(opts ...GRIBIOption) func(proto.Message) error { 142 | return func(msg proto.Message) error { 143 | if msg == nil { 144 | return ErrInvalidMsgType 145 | } 146 | switch msg := msg.ProtoReflect().Interface().(type) { 147 | case *spb.AFTOperation: 148 | nh := new(gribi_aft.Afts_NextHopKey) 149 | err := apply(nh, opts...) 150 | if err != nil { 151 | return err 152 | } 153 | msg.Entry = &spb.AFTOperation_NextHop{ 154 | NextHop: nh, 155 | } 156 | return nil 157 | default: 158 | return fmt.Errorf("option EntryNH: %w: %T", ErrInvalidMsgType, msg) 159 | } 160 | } 161 | } 162 | 163 | func Index(index uint64) func(proto.Message) error { 164 | return func(msg proto.Message) error { 165 | if msg == nil { 166 | return ErrInvalidMsgType 167 | } 168 | switch msg := msg.ProtoReflect().Interface().(type) { 169 | case *gribi_aft.Afts_NextHopKey: 170 | msg.Index = index 171 | return nil 172 | default: 173 | return fmt.Errorf("option Index: %w: %T", ErrInvalidMsgType, msg) 174 | } 175 | } 176 | } 177 | 178 | func IPAddress(ipAddr string) func(proto.Message) error { 179 | return func(msg proto.Message) error { 180 | if msg == nil { 181 | return ErrInvalidMsgType 182 | } 183 | switch msg := msg.ProtoReflect().Interface().(type) { 184 | case *gribi_aft.Afts_NextHopKey: 185 | if ipAddr == "" { 186 | return nil 187 | } 188 | if msg.NextHop == nil { 189 | msg.NextHop = new(gribi_aft.Afts_NextHop) 190 | } 191 | msg.NextHop.IpAddress = &ywrapper.StringValue{Value: ipAddr} 192 | default: 193 | return fmt.Errorf("option IPAddress: %w: %T", ErrInvalidMsgType, msg) 194 | } 195 | return nil 196 | } 197 | } 198 | 199 | func DecapsulateHeader(typ string) func(proto.Message) error { 200 | return func(msg proto.Message) error { 201 | if msg == nil { 202 | return ErrInvalidMsgType 203 | } 204 | if typ == "" { 205 | return nil 206 | } 207 | switch msg := msg.ProtoReflect().Interface().(type) { 208 | case *gribi_aft.Afts_NextHopKey: 209 | if msg.NextHop == nil { 210 | msg.NextHop = new(gribi_aft.Afts_NextHop) 211 | } 212 | switch strings.ToUpper(typ) { 213 | case "GRE": 214 | msg.NextHop.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_GRE 215 | case "IPV4": 216 | msg.NextHop.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV4 217 | case "IPV6": 218 | msg.NextHop.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV6 219 | case "MPLS": 220 | msg.NextHop.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_MPLS 221 | default: 222 | return fmt.Errorf("option DecapsulateHeader: %w: %T", ErrInvalidValue, msg) 223 | } 224 | case *gribi_aft.Afts_Ipv4EntryKey: 225 | if msg.Ipv4Entry == nil { 226 | msg.Ipv4Entry = new(gribi_aft.Afts_Ipv4Entry) 227 | } 228 | switch strings.ToUpper(typ) { 229 | case "GRE": 230 | msg.Ipv4Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_GRE 231 | case "IPV4": 232 | msg.Ipv4Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV4 233 | case "IPV6": 234 | msg.Ipv4Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV6 235 | case "MPLS": 236 | msg.Ipv4Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_MPLS 237 | default: 238 | return fmt.Errorf("option DecapsulateHeader: %w: %T", ErrInvalidValue, msg) 239 | } 240 | case *gribi_aft.Afts_Ipv6EntryKey: 241 | if msg.Ipv6Entry == nil { 242 | msg.Ipv6Entry = new(gribi_aft.Afts_Ipv6Entry) 243 | } 244 | switch strings.ToUpper(typ) { 245 | case "GRE": 246 | msg.Ipv6Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_GRE 247 | case "IPV4": 248 | msg.Ipv6Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV4 249 | case "IPV6": 250 | msg.Ipv6Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV6 251 | case "MPLS": 252 | msg.Ipv6Entry.DecapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_MPLS 253 | default: 254 | return fmt.Errorf("option DecapsulateHeader: %w: %T", ErrInvalidValue, msg) 255 | } 256 | default: 257 | return fmt.Errorf("option DecapsulateHeader: %w: %T", ErrInvalidMsgType, msg) 258 | } 259 | return nil 260 | } 261 | } 262 | 263 | func DecapsulateHeaderGRE() func(proto.Message) error { 264 | return DecapsulateHeader("GRE") 265 | } 266 | 267 | func DecapsulateHeaderIPv4() func(proto.Message) error { 268 | return DecapsulateHeader("IPV4") 269 | } 270 | 271 | func DecapsulateHeaderIPv6() func(proto.Message) error { 272 | return DecapsulateHeader("IPV6") 273 | } 274 | 275 | func DecapsulateHeaderMPLS() func(proto.Message) error { 276 | return DecapsulateHeader("MPLS") 277 | } 278 | 279 | func EncapsulateHeader(typ string) func(proto.Message) error { 280 | return func(msg proto.Message) error { 281 | if msg == nil { 282 | return ErrInvalidMsgType 283 | } 284 | if typ == "" { 285 | return nil 286 | } 287 | switch msg := msg.ProtoReflect().Interface().(type) { 288 | case *gribi_aft.Afts_NextHopKey: 289 | if msg.NextHop == nil { 290 | msg.NextHop = new(gribi_aft.Afts_NextHop) 291 | } 292 | switch strings.ToUpper(typ) { 293 | case "GRE": 294 | msg.NextHop.EncapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_GRE 295 | case "IPV4": 296 | msg.NextHop.EncapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV4 297 | case "IPV6": 298 | msg.NextHop.EncapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_IPV6 299 | case "MPLS": 300 | msg.NextHop.EncapsulateHeader = enums.OpenconfigAftTypesEncapsulationHeaderType_OPENCONFIGAFTTYPESENCAPSULATIONHEADERTYPE_MPLS 301 | default: 302 | return fmt.Errorf("option EncapsulateHeader: %w: %T", ErrInvalidValue, msg) 303 | } 304 | default: 305 | return fmt.Errorf("option EncapsulateHeader: %w: %T", ErrInvalidMsgType, msg) 306 | } 307 | return nil 308 | } 309 | } 310 | 311 | func EncapsulateHeaderGRE() func(proto.Message) error { 312 | return EncapsulateHeader("GRE") 313 | } 314 | 315 | func EncapsulateHeaderIPv4() func(proto.Message) error { 316 | return EncapsulateHeader("IPV4") 317 | } 318 | 319 | func EncapsulateHeaderIPv6() func(proto.Message) error { 320 | return EncapsulateHeader("IPV6") 321 | } 322 | 323 | func EncapsulateHeaderMPLS() func(proto.Message) error { 324 | return EncapsulateHeader("MPLS") 325 | } 326 | 327 | func Interface(iface string) func(proto.Message) error { 328 | return func(msg proto.Message) error { 329 | if msg == nil { 330 | return ErrInvalidMsgType 331 | } 332 | if iface == "" { 333 | return nil 334 | } 335 | switch msg := msg.ProtoReflect().Interface().(type) { 336 | case *gribi_aft.Afts_NextHopKey: 337 | if msg.NextHop == nil { 338 | msg.NextHop = new(gribi_aft.Afts_NextHop) 339 | } 340 | if msg.NextHop.InterfaceRef == nil { 341 | msg.NextHop.InterfaceRef = new(gribi_aft.Afts_NextHop_InterfaceRef) 342 | } 343 | msg.NextHop.InterfaceRef.Interface = &ywrapper.StringValue{Value: iface} 344 | default: 345 | return fmt.Errorf("option Interface: %w: %T", ErrInvalidMsgType, msg) 346 | } 347 | return nil 348 | } 349 | } 350 | 351 | func SubInterface(subIface uint64) func(proto.Message) error { 352 | return func(msg proto.Message) error { 353 | if msg == nil { 354 | return ErrInvalidMsgType 355 | } 356 | switch msg := msg.ProtoReflect().Interface().(type) { 357 | case *gribi_aft.Afts_NextHopKey: 358 | if msg.NextHop == nil { 359 | msg.NextHop = new(gribi_aft.Afts_NextHop) 360 | } 361 | if msg.NextHop.InterfaceRef == nil { 362 | msg.NextHop.InterfaceRef = new(gribi_aft.Afts_NextHop_InterfaceRef) 363 | } 364 | msg.NextHop.InterfaceRef.Subinterface = &ywrapper.UintValue{Value: subIface} 365 | default: 366 | return fmt.Errorf("option SubInterface: %w: %T", ErrInvalidMsgType, msg) 367 | } 368 | return nil 369 | } 370 | } 371 | 372 | func IPinIP(src, dst string) func(proto.Message) error { 373 | return func(msg proto.Message) error { 374 | if msg == nil { 375 | return ErrInvalidMsgType 376 | } 377 | switch msg := msg.ProtoReflect().Interface().(type) { 378 | case *gribi_aft.Afts_NextHopKey: 379 | if msg.NextHop == nil { 380 | msg.NextHop = new(gribi_aft.Afts_NextHop) 381 | } 382 | if msg.NextHop.IpInIp == nil { 383 | msg.NextHop.IpInIp = new(gribi_aft.Afts_NextHop_IpInIp) 384 | } 385 | if src != "" { 386 | msg.NextHop.IpInIp.SrcIp = &ywrapper.StringValue{Value: src} 387 | } 388 | if dst != "" { 389 | msg.NextHop.IpInIp.DstIp = &ywrapper.StringValue{Value: dst} 390 | } 391 | default: 392 | return fmt.Errorf("option IPinIP: %w: %T", ErrInvalidMsgType, msg) 393 | } 394 | return nil 395 | } 396 | } 397 | 398 | func MAC(mac string) func(proto.Message) error { 399 | return func(msg proto.Message) error { 400 | if msg == nil { 401 | return ErrInvalidMsgType 402 | } 403 | if mac == "" { 404 | return nil 405 | } 406 | switch msg := msg.ProtoReflect().Interface().(type) { 407 | case *gribi_aft.Afts_NextHopKey: 408 | if msg.NextHop == nil { 409 | msg.NextHop = new(gribi_aft.Afts_NextHop) 410 | } 411 | msg.NextHop.MacAddress = &ywrapper.StringValue{Value: mac} 412 | default: 413 | return fmt.Errorf("option MAC: %w: %T", ErrInvalidMsgType, msg) 414 | } 415 | return nil 416 | } 417 | } 418 | 419 | func PushedMplsLabelStack(typ string, label uint64) func(proto.Message) error { 420 | return func(msg proto.Message) error { 421 | if msg == nil { 422 | return ErrInvalidMsgType 423 | } 424 | switch msg := msg.ProtoReflect().Interface().(type) { 425 | case *gribi_aft.Afts_NextHopKey: 426 | if msg.NextHop == nil { 427 | msg.NextHop = new(gribi_aft.Afts_NextHop) 428 | } 429 | if msg.NextHop.PushedMplsLabelStack == nil { 430 | msg.NextHop.PushedMplsLabelStack = make([]*gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion, 0) 431 | } 432 | typ = strings.ToUpper(typ) 433 | typ = strings.ReplaceAll(typ, "-", "_") 434 | switch typ { 435 | case "IPV4_EXPLICIT_NULL": 436 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 437 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 438 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_IPV4_EXPLICIT_NULL, 439 | PushedMplsLabelStackUint64: label, 440 | }) 441 | case "ROUTER_ALERT": 442 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 443 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 444 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_ROUTER_ALERT, 445 | PushedMplsLabelStackUint64: label, 446 | }) 447 | case "IPV6_EXPLICIT_NULL": 448 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 449 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 450 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_IPV6_EXPLICIT_NULL, 451 | PushedMplsLabelStackUint64: label, 452 | }) 453 | case "IMPLICIT_NULL": 454 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 455 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 456 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_IMPLICIT_NULL, 457 | PushedMplsLabelStackUint64: label, 458 | }) 459 | case "ENTROPY_LABEL_INDICATOR": 460 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 461 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 462 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_ENTROPY_LABEL_INDICATOR, 463 | PushedMplsLabelStackUint64: label, 464 | }) 465 | case "NO_LABEL": 466 | msg.NextHop.PushedMplsLabelStack = append(msg.NextHop.PushedMplsLabelStack, 467 | &gribi_aft.Afts_NextHop_PushedMplsLabelStackUnion{ 468 | PushedMplsLabelStackOpenconfigmplstypesmplslabelenum: enums.OpenconfigMplsTypesMplsLabelEnum_OPENCONFIGMPLSTYPESMPLSLABELENUM_NO_LABEL, 469 | // PushedMplsLabelStackUint64: label, 470 | }) 471 | default: 472 | return fmt.Errorf("option PushedMplsLabelStack: %w: %T", ErrInvalidValue, msg) 473 | } 474 | default: 475 | return fmt.Errorf("option PushedMplsLabelStack: %w: %T", ErrInvalidMsgType, msg) 476 | } 477 | return nil 478 | } 479 | } 480 | 481 | func PushedMplsLabelStackIPv4(label uint64) func(proto.Message) error { 482 | return PushedMplsLabelStack("IPV4_EXPLICIT_NULL", label) 483 | } 484 | 485 | func PushedMplsLabelStackRouterAlert(label uint64) func(proto.Message) error { 486 | return PushedMplsLabelStack("ROUTER_ALERT", label) 487 | } 488 | 489 | func PushedMplsLabelStackRouterIPv6(label uint64) func(proto.Message) error { 490 | return PushedMplsLabelStack("IPV6_EXPLICIT_NULL", label) 491 | } 492 | 493 | func PushedMplsLabelStackRouterImplicit(label uint64) func(proto.Message) error { 494 | return PushedMplsLabelStack("IMPLICIT_NULL", label) 495 | } 496 | 497 | func PushedMplsLabelStackRouterEntropy(label uint64) func(proto.Message) error { 498 | return PushedMplsLabelStack("ENTROPY_LABEL_INDICATOR", label) 499 | } 500 | 501 | func PushedMplsLabelStackRouterNoLabel() func(proto.Message) error { 502 | return PushedMplsLabelStack("NO_LABEL", 0) 503 | } 504 | 505 | // Next Hop Group Options 506 | func NHGEntry(opts ...GRIBIOption) func(proto.Message) error { 507 | return func(msg proto.Message) error { 508 | if msg == nil { 509 | return ErrInvalidMsgType 510 | } 511 | switch msg := msg.ProtoReflect().Interface().(type) { 512 | case *spb.AFTOperation: 513 | nhg := new(gribi_aft.Afts_NextHopGroupKey) 514 | err := apply(nhg, opts...) 515 | if err != nil { 516 | return err 517 | } 518 | msg.Entry = &spb.AFTOperation_NextHopGroup{ 519 | NextHopGroup: nhg, 520 | } 521 | return nil 522 | default: 523 | return fmt.Errorf("option EntryNHG: %w: %T", ErrInvalidMsgType, msg) 524 | } 525 | } 526 | } 527 | 528 | func BackupNextHopGroup(index uint64) func(proto.Message) error { 529 | return func(msg proto.Message) error { 530 | if msg == nil { 531 | return ErrInvalidMsgType 532 | } 533 | switch msg := msg.ProtoReflect().Interface().(type) { 534 | case *gribi_aft.Afts_NextHopGroupKey: 535 | if msg.NextHopGroup == nil { 536 | msg.NextHopGroup = new(gribi_aft.Afts_NextHopGroup) 537 | } 538 | msg.NextHopGroup.BackupNextHopGroup = &ywrapper.UintValue{Value: index} 539 | default: 540 | return fmt.Errorf("option BackupNextHopGroup: %w: %T", ErrInvalidMsgType, msg) 541 | } 542 | return nil 543 | } 544 | } 545 | 546 | func Color(index uint64) func(proto.Message) error { 547 | return func(msg proto.Message) error { 548 | if msg == nil { 549 | return ErrInvalidMsgType 550 | } 551 | switch msg := msg.ProtoReflect().Interface().(type) { 552 | case *gribi_aft.Afts_NextHopGroupKey: 553 | if msg.NextHopGroup == nil { 554 | msg.NextHopGroup = new(gribi_aft.Afts_NextHopGroup) 555 | } 556 | msg.NextHopGroup.Color = &ywrapper.UintValue{Value: index} 557 | default: 558 | return fmt.Errorf("option Color: %w: %T", ErrInvalidMsgType, msg) 559 | } 560 | return nil 561 | } 562 | } 563 | 564 | func NHGNextHop(index, weight uint64) func(proto.Message) error { 565 | return func(msg proto.Message) error { 566 | if msg == nil { 567 | return ErrInvalidMsgType 568 | } 569 | switch msg := msg.ProtoReflect().Interface().(type) { 570 | case *gribi_aft.Afts_NextHopGroupKey: 571 | if msg.NextHopGroup == nil { 572 | msg.NextHopGroup = new(gribi_aft.Afts_NextHopGroup) 573 | } 574 | if len(msg.NextHopGroup.NextHop) == 0 { 575 | msg.NextHopGroup.NextHop = make([]*gribi_aft.Afts_NextHopGroup_NextHopKey, 0) 576 | } 577 | nhgnh := new(gribi_aft.Afts_NextHopGroup_NextHopKey) 578 | nhgnh.Index = index 579 | if weight > 0 { 580 | nhgnh.NextHop = &gribi_aft.Afts_NextHopGroup_NextHop{ 581 | Weight: &ywrapper.UintValue{Value: weight}, 582 | } 583 | } 584 | msg.NextHopGroup.NextHop = append(msg.NextHopGroup.NextHop, nhgnh) 585 | default: 586 | return fmt.Errorf("option NHGNextHop: %w: %T", ErrInvalidMsgType, msg) 587 | } 588 | return nil 589 | } 590 | } 591 | 592 | // IPv4 Options 593 | func IPv4Entry(opts ...GRIBIOption) func(proto.Message) error { 594 | return func(msg proto.Message) error { 595 | if msg == nil { 596 | return ErrInvalidMsgType 597 | } 598 | switch msg := msg.ProtoReflect().Interface().(type) { 599 | case *spb.AFTOperation: 600 | ipv4 := new(gribi_aft.Afts_Ipv4EntryKey) 601 | err := apply(ipv4, opts...) 602 | if err != nil { 603 | return err 604 | } 605 | msg.Entry = &spb.AFTOperation_Ipv4{ 606 | Ipv4: ipv4, 607 | } 608 | return nil 609 | default: 610 | return fmt.Errorf("option IPv4Entry: %w: %T", ErrInvalidMsgType, msg) 611 | } 612 | } 613 | } 614 | 615 | func Prefix(prefix string) func(proto.Message) error { 616 | return func(msg proto.Message) error { 617 | if msg == nil { 618 | return ErrInvalidMsgType 619 | } 620 | switch msg := msg.ProtoReflect().Interface().(type) { 621 | case *gribi_aft.Afts_Ipv4EntryKey: 622 | msg.Prefix = prefix 623 | case *gribi_aft.Afts_Ipv6EntryKey: 624 | msg.Prefix = prefix 625 | default: 626 | return fmt.Errorf("option Prefix: %w: %T", ErrInvalidMsgType, msg) 627 | } 628 | return nil 629 | } 630 | } 631 | 632 | func Metadata(md []byte) func(proto.Message) error { 633 | return func(msg proto.Message) error { 634 | if msg == nil { 635 | return ErrInvalidMsgType 636 | } 637 | if len(md) == 0 { 638 | return nil 639 | } 640 | switch msg := msg.ProtoReflect().Interface().(type) { 641 | case *gribi_aft.Afts_Ipv4EntryKey: 642 | if msg.Ipv4Entry == nil { 643 | msg.Ipv4Entry = new(gribi_aft.Afts_Ipv4Entry) 644 | } 645 | msg.Ipv4Entry.EntryMetadata = &ywrapper.BytesValue{Value: md} 646 | case *gribi_aft.Afts_Ipv6EntryKey: 647 | if msg.Ipv6Entry == nil { 648 | msg.Ipv6Entry = new(gribi_aft.Afts_Ipv6Entry) 649 | } 650 | msg.Ipv6Entry.EntryMetadata = &ywrapper.BytesValue{Value: md} 651 | default: 652 | return fmt.Errorf("option Metadata: %w: %T", ErrInvalidMsgType, msg) 653 | } 654 | return nil 655 | } 656 | } 657 | 658 | func NHG(id uint64) func(proto.Message) error { 659 | return func(msg proto.Message) error { 660 | if msg == nil { 661 | return ErrInvalidMsgType 662 | } 663 | switch msg := msg.ProtoReflect().Interface().(type) { 664 | case *gribi_aft.Afts_Ipv4EntryKey: 665 | if msg.Ipv4Entry == nil { 666 | msg.Ipv4Entry = new(gribi_aft.Afts_Ipv4Entry) 667 | } 668 | msg.Ipv4Entry.NextHopGroup = &ywrapper.UintValue{Value: id} 669 | case *gribi_aft.Afts_Ipv6EntryKey: 670 | if msg.Ipv6Entry == nil { 671 | msg.Ipv6Entry = new(gribi_aft.Afts_Ipv6Entry) 672 | } 673 | msg.Ipv6Entry.NextHopGroup = &ywrapper.UintValue{Value: id} 674 | default: 675 | return fmt.Errorf("option NHG: %w: %T", ErrInvalidMsgType, msg) 676 | } 677 | return nil 678 | } 679 | } 680 | 681 | // IPv6 Entry Options 682 | func IPv6Entry(opts ...GRIBIOption) func(proto.Message) error { 683 | return func(msg proto.Message) error { 684 | if msg == nil { 685 | return ErrInvalidMsgType 686 | } 687 | switch msg := msg.ProtoReflect().Interface().(type) { 688 | case *spb.AFTOperation: 689 | ipv6 := new(gribi_aft.Afts_Ipv6EntryKey) 690 | err := apply(ipv6, opts...) 691 | if err != nil { 692 | return err 693 | } 694 | msg.Entry = &spb.AFTOperation_Ipv6{ 695 | Ipv6: ipv6, 696 | } 697 | return nil 698 | default: 699 | return fmt.Errorf("option IPv6Entry: %w: %T", ErrInvalidMsgType, msg) 700 | } 701 | } 702 | } 703 | --------------------------------------------------------------------------------