Skip to content

Commit

Permalink
Merge pull request #12 from colinmcintosh/feature/netbox
Browse files Browse the repository at this point in the history
Added NetBox target loader
  • Loading branch information
colinmcintosh authored Oct 18, 2020
2 parents 4794471 + 785ecd0 commit d6b0287
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 3 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ You must also sign a one-time CLA for any pull requests to be accepted. See
[CONTRIBUTING.md](./CONTRIBUTING.md) for details.


## Troubleshooting

#### "`context deadline exceeded`" Error

If you see a `context deadline exceeded` error from the connection manager it
means there is some underlying issue that is causing the connection to a target
to fail. This seems to often be a TLS issue (wrong certs, bad config, etc) but
it could be something else. Try running gnmi-gateway with gRPC connection
logging enabled. For example:

```bash
GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info ./gnmi-gateway
```


[1]: https://github.com/openconfig/gnmi
[2]: https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#35-subscribing-to-telemetry-updates
[3]: https://zookeeper.apache.org/
Expand Down
26 changes: 25 additions & 1 deletion gateway/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (
type GatewayConfig struct {
// ClientTLSConfig are the gNMI client TLS credentials. Setting this will enable client TLS.
// TODO (cmcintosh): Add options to set client certificates by path (i.e. like the server TLS creds).
ClientTLSConfig *tls.Config
ClientTLSConfig *tls.Config `ignored:"true"`
// EnableGNMIServer will run the gNMI server (the Subscribe server). TLS options are also required
// for the gNMI server to be enabled.
EnableGNMIServer bool `json:"enable_gnmi_server"`
Expand Down Expand Up @@ -140,6 +140,30 @@ type TargetLoadersConfig struct {
// JSONFileReloadInterval is the interval to check TargetJSONFile for changes.
JSONFileReloadInterval time.Duration `json:"json_file_reload_interval"`

// NetBoxAPIKey is a valid API for the NetBox instance.
NetBoxAPIKey string
// NetBoxDeviceGNMIPort is the port on the device that the gNMI server is
// running on.
NetBoxDeviceGNMIPort int
// NetBoxDeviceUsername is the username of the devices to connect to.
// TODO (cmcintosh): replace this with NetBox secrets
NetBoxDeviceUsername string
// NetBoxDevicePossword is the passowrd of the devices to connect to.
// TODO (cmcintosh): replace this with NetBox secrets
NetBoxDevicePassword string
// NetBoxHost is the address and port of the NetBox server.
NetBoxHost string
// NetBoxIncludeTag is a tag to filter devices in NetBox by. If no tag is
// supplied all devices in NetBox with valid details are included. You
// probably want to set this unless all of your devices support gNMI.
NetBoxIncludeTag string
// NetBoxReloadInterval is the frequency at which to check NetBox for
// changes to devices.
NetBoxReloadInterval time.Duration
// NetBoxSubscribePaths is a list of XPath subscription paths for the
// devices in NetBox.
NetBoxSubscribePaths []string

// SimpleFile is the path to a YAML file containing a simple target config.
SimpleFile string `json:"simple_file"`
// SimpleFileReloadInterval is the interval to check SimpleFile for changes.
Expand Down
1 change: 1 addition & 0 deletions gateway/loaders/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
package all

import _ "github.com/openconfig/gnmi-gateway/gateway/loaders/json"
import _ "github.com/openconfig/gnmi-gateway/gateway/loaders/netbox"
import _ "github.com/openconfig/gnmi-gateway/gateway/loaders/simple"
163 changes: 163 additions & 0 deletions gateway/loaders/netbox/netbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2020 Netflix Inc
// Author: Colin McIntosh ([email protected])
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package netbox provides a TargetLoader for loading devices from NetBox.
package netbox

import (
"context"
"fmt"
"github.com/google/gnxi/utils/xpath"
"github.com/netbox-community/go-netbox/netbox"
"github.com/netbox-community/go-netbox/netbox/client"
"github.com/netbox-community/go-netbox/netbox/client/dcim"
"github.com/openconfig/gnmi-gateway/gateway/configuration"
"github.com/openconfig/gnmi-gateway/gateway/connections"
"github.com/openconfig/gnmi-gateway/gateway/loaders"
"github.com/openconfig/gnmi/proto/gnmi"
targetpb "github.com/openconfig/gnmi/proto/target"
"github.com/openconfig/gnmi/target"
"net"
"strconv"
"time"
)

const Name = "netbox"

var _ loaders.TargetLoader = new(NetBoxTargetLoader)

type NetBoxTargetLoader struct {
config *configuration.GatewayConfig
last *targetpb.Configuration
apiKey string
client *client.NetBoxAPI
host string
interval time.Duration
}

func init() {
loaders.Register(Name, NewNetBoxTargetLoader)
}

func NewNetBoxTargetLoader(config *configuration.GatewayConfig) loaders.TargetLoader {
return &NetBoxTargetLoader{
config: config,
apiKey: config.TargetLoaders.NetBoxAPIKey,
host: config.TargetLoaders.NetBoxHost,
interval: config.TargetLoaders.NetBoxReloadInterval,
}
}

func (m *NetBoxTargetLoader) GetConfiguration() (*targetpb.Configuration, error) {
resp, err := m.client.Dcim.DcimDevicesList(&dcim.DcimDevicesListParams{
Context: context.Background(),
Tag: &m.config.TargetLoaders.NetBoxIncludeTag,
}, nil)
if err != nil {
if resp != nil {
m.config.Log.Error().Msgf("NetBox devices list response: %s", resp.Error())
}
err = fmt.Errorf("unable to list devices in NetBox with tag '%s': %v", m.config.TargetLoaders.NetBoxIncludeTag, err)
m.config.Log.Error().Msg(err.Error())
return nil, err
}

configs := &targetpb.Configuration{
Target: make(map[string]*targetpb.Target),
Request: make(map[string]*gnmi.SubscribeRequest),
}

var subs []*gnmi.Subscription
for _, x := range m.config.TargetLoaders.NetBoxSubscribePaths {
path, err := xpath.ToGNMIPath(x)
if err != nil {
return nil, fmt.Errorf("unable to parse simple config XPath: %s: %v", x, err)
}
subs = append(subs, &gnmi.Subscription{Path: path})
}
configs.Request["default"] = &gnmi.SubscribeRequest{
Request: &gnmi.SubscribeRequest_Subscribe{
Subscribe: &gnmi.SubscriptionList{
Prefix: &gnmi.Path{},
Subscription: subs,
},
},
}

payload := resp.GetPayload()
if payload != nil {
for _, device := range payload.Results {
if device.PrimaryIP.Address == nil || *device.PrimaryIP.Address == "" {
continue
}
ip, _, err := net.ParseCIDR(*device.PrimaryIP.Address)
if err != nil {
m.config.Log.Error().Msgf("unable to parse IP for NetBox device %s: %v", device.Name, err)
continue
}

ipBytes, _ := ip.MarshalText()
address := string(ipBytes) + ":" + strconv.Itoa(m.config.TargetLoaders.NetBoxDeviceGNMIPort)

configs.Target[*device.Name] = &targetpb.Target{
Addresses: []string{address},
Request: "default",
Credentials: &targetpb.Credentials{
Username: m.config.TargetLoaders.NetBoxDeviceUsername,
Password: m.config.TargetLoaders.NetBoxDevicePassword,
},
}
}
}

if err := target.Validate(configs); err != nil {
return nil, fmt.Errorf("configuration from NetBox loader is invalid: %w", err)
}
return configs, nil
}

func (m *NetBoxTargetLoader) Start() error {
m.client = netbox.NewNetboxWithAPIKey(
m.config.TargetLoaders.NetBoxHost,
m.config.TargetLoaders.NetBoxAPIKey,
)

_, err := m.GetConfiguration() // make sure there are no errors at startup
return err
}

func (m *NetBoxTargetLoader) WatchConfiguration(targetChan chan<- *connections.TargetConnectionControl) error {
for {
targetConfig, err := m.GetConfiguration()
if err != nil {
m.config.Log.Error().Err(err).Msgf("Unable to get target configuration.")
} else {
controlMsg := new(connections.TargetConnectionControl)
if m.last != nil {
for targetName := range m.last.Target {
_, exists := targetConfig.Target[targetName]
if !exists {
controlMsg.Remove = append(controlMsg.Remove, targetName)
}
}
}
controlMsg.Insert = targetConfig
m.last = targetConfig

targetChan <- controlMsg
}
time.Sleep(m.interval)
}
}
11 changes: 10 additions & 1 deletion gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ func ParseArgs(config *configuration.GatewayConfig) error {
configFile := flag.String("ConfigFile", "", "Path of the gateway configuration JSON file.")
flag.BoolVar(&config.EnableGNMIServer, "EnableGNMIServer", false, "Enable the gNMI server")
exporters := flag.String("Exporters", "", "Comma-separated list of Exporters to enable.")

flag.Int64Var(&config.Exporters.KafkaBatchBytes, "ExporterKafkaBatchBytes", 1048576, "Max bytes that will be buffered before flushing messages to a Kafka partition")
flag.IntVar(&config.Exporters.KafkaBatchSize, "ExporterKafkaBatchSize", 10000, "Max number of messages that will be buffered before flushing messages to a Kafka partition")
flag.DurationVar(&config.Exporters.KafkaBatchTimeout, "ExporterKafkaBatchTimeout", 1*time.Second, "Max seconds between flushing messages to a Kafka partition")
exporterKafkaBrokers := flag.String("ExporterKafkaBrokers", "", "Comma-separated list of Kafka broker addresses and ports for the Kafka Exporter to connect to")
flag.BoolVar(&config.Exporters.KafkaLogging, "ExporterKafkaLogging", false, "Enables info level logging from the Kafka writer. Error level logging is always enabled")
flag.StringVar(&config.Exporters.KafkaTopic, "ExporterKafkaTopic", "", "Kafka topic to send exported gNMI messages to.")

flag.Uint64Var(&config.GatewayTransitionBufferSize, "GatewayTransitionBufferSize", 100000, "Tunes the size of the buffer between targets and exporters/clients")
flag.BoolVar(&config.LogCaller, "LogCaller", false, "Include the file and line number with each log message")
flag.StringVar(&config.OpenConfigDirectory, "OpenConfigDirectory", "", "Directory (required to enable Prometheus exporter)")
Expand All @@ -116,6 +116,14 @@ func ParseArgs(config *configuration.GatewayConfig) error {
flag.DurationVar(&config.TargetLoaders.JSONFileReloadInterval, "TargetJSONFileReloadInterval", 30*time.Second, "Interval to reload the JSON file containing the target configurations")
flag.DurationVar(&config.TargetDialTimeout, "TargetDialTimeout", 10*time.Second, "Dial timeout time")
flag.IntVar(&config.TargetLimit, "TargetLimit", 100, "Maximum number of targets that this instance will connect to at once")
flag.StringVar(&config.TargetLoaders.NetBoxAPIKey, "TargetNetBoxAPIKey", "", "API Key for NetBox target loader")
flag.IntVar(&config.TargetLoaders.NetBoxDeviceGNMIPort, "TargetNetBoxDeviceGNMIPort", 0, "The port that the gNMI is served from on devices loaded from NetBox ")
flag.StringVar(&config.TargetLoaders.NetBoxDeviceUsername, "TargetNetBoxDeviceUsername", "", "The port that the gNMI is served from on devices loaded from NetBox ")
flag.StringVar(&config.TargetLoaders.NetBoxDevicePassword, "TargetNetBoxDevicePassword", "", "The port that the gNMI is served from on devices loaded from NetBox ")
flag.StringVar(&config.TargetLoaders.NetBoxHost, "TargetNetBoxHost", "", "The address and port where the NetBox API can be reached")
flag.StringVar(&config.TargetLoaders.NetBoxIncludeTag, "TargetNetBoxIncludeTag", "", "A tag to filter devices loaded from NetBox")
flag.DurationVar(&config.TargetLoaders.NetBoxReloadInterval, "TargetNetBoxReloadInterval", 3*time.Minute, "The frequency at which to check NetBox for new or changed devices.")
netboxSubscribePaths := flag.String("TargetNetBoxSubscribePaths", "", "Comma separated (no spaces) list of paths to subscribe to for devices loaded from NetBox")
zkHosts := flag.String("ZookeeperHosts", "", "Comma separated (no spaces) list of zookeeper hosts including port")
flag.StringVar(&config.ZookeeperPrefix, "ZookeeperPrefix", "/gnmi/gateway/", "Prefix for the lock path in Zookeeper")
flag.DurationVar(&config.ZookeeperTimeout, "ZookeeperTimeout", 1*time.Second, "Zookeeper timeout time. Minimum is 1 second. Failover time is (ZookeeperTimeout * 2)")
Expand All @@ -124,6 +132,7 @@ func ParseArgs(config *configuration.GatewayConfig) error {
config.Exporters.Enabled = cleanSplit(*exporters)
config.Exporters.KafkaBrokers = cleanSplit(*exporterKafkaBrokers)
config.TargetLoaders.Enabled = cleanSplit(*targetLoaders)
config.TargetLoaders.NetBoxSubscribePaths = cleanSplit(*netboxSubscribePaths)
config.ZookeeperHosts = cleanSplit(*zkHosts)

if *configFile != "" {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ require (
github.com/google/gnxi v0.0.0-20200831120455-017df4756f78
github.com/google/go-cmp v0.5.2
github.com/kelseyhightower/envconfig v1.4.0
github.com/netbox-community/go-netbox v0.0.0-20201002085217-91e5d561efe4
github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802
github.com/openconfig/goyang v0.0.0-20200623182805-6be32aef2bcd
github.com/prometheus/client_golang v1.4.1
github.com/rs/zerolog v1.17.2
github.com/segmentio/kafka-go v0.4.6
github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.6.1
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
google.golang.org/grpc v1.30.0
gopkg.in/yaml.v2 v2.3.0
Expand Down
Loading

0 comments on commit d6b0287

Please sign in to comment.