bird_exporter-1.2.2/000077500000000000000000000000001333773000500143655ustar00rootroot00000000000000bird_exporter-1.2.2/.gitignore000066400000000000000000000000321333773000500163500ustar00rootroot00000000000000.idea *.iml bird_exporter bird_exporter-1.2.2/.travis.yml000066400000000000000000000000711333773000500164740ustar00rootroot00000000000000language: go go: - 1.8 script: make init test vet lint bird_exporter-1.2.2/Dockerfile000066400000000000000000000006301333773000500163560ustar00rootroot00000000000000from golang:1.10 as builder arg CMD run wget -o/dev/null -O/usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64 && \ chmod +x /usr/local/bin/dep workdir ${GOPATH}/src/github.com/czerwonk/bird_exporter copy . . run make deps build && cp bird_exporter /bird_exporter from golang:1.10 copy --from=builder /bird_exporter /bird_exporter entrypoint ["/bird_exporter"] bird_exporter-1.2.2/Gopkg.lock000066400000000000000000000052451333773000500163140ustar00rootroot00000000000000# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. [[projects]] branch = "master" name = "github.com/alecthomas/template" packages = [".","parse"] revision = "a0175ee3bccc567396460bf5acd36800cb10c49c" [[projects]] branch = "master" name = "github.com/alecthomas/units" packages = ["."] revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" [[projects]] branch = "master" name = "github.com/beorn7/perks" packages = ["quantile"] revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] branch = "master" name = "github.com/czerwonk/bird_socket" packages = ["."] revision = "fe8194eb5598e088bb0a64174c1a1da6eeeb4fef" [[projects]] branch = "master" name = "github.com/czerwonk/testutils" packages = ["assert"] revision = "dd9dabe360d49188fd1194907dcf0738cbca1858" [[projects]] branch = "master" name = "github.com/golang/protobuf" packages = ["proto"] revision = "130e6b02ab059e7b717a096f397c5b60111cae74" [[projects]] name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] name = "github.com/prometheus/client_golang" packages = ["prometheus","prometheus/promhttp"] revision = "c5b7fccd204277076155f10851dad72b76a49317" version = "v0.8.0" [[projects]] branch = "master" name = "github.com/prometheus/client_model" packages = ["go"] revision = "6f3806018612930941127f2a7c6c453ba2c527d2" [[projects]] branch = "master" name = "github.com/prometheus/common" packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","log","model"] revision = "2f17f4a9d485bf34b4bfaccc273805040e4f86c8" [[projects]] branch = "master" name = "github.com/prometheus/procfs" packages = [".","xfs"] revision = "e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2" [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e" version = "v1.0.3" [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["ssh/terminal"] revision = "76eec36fa14229c4b25bb894c2d0e591527af429" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix","windows","windows/registry","windows/svc/eventlog"] revision = "314a259e304ff91bd6985da2a7149bbf91237993" [[projects]] name = "gopkg.in/alecthomas/kingpin.v2" packages = ["."] revision = "1087e65c9441605df944fb12c33f0fe7072d18ca" version = "v2.2.5" [solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "7a2645f79517b39461869d1e2f5071eb7509e85d3ee6ad23d19892d6387542e7" solver-name = "gps-cdcl" solver-version = 1 bird_exporter-1.2.2/Gopkg.toml000066400000000000000000000015221333773000500163310ustar00rootroot00000000000000 # Gopkg.toml example # # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md # for detailed Gopkg.toml documentation. # # required = ["github.com/user/thing/cmd/thing"] # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] # # [[constraint]] # name = "github.com/user/project" # version = "1.0.0" # # [[constraint]] # name = "github.com/user/project2" # branch = "dev" # source = "github.com/myfork/project2" # # [[override]] # name = "github.com/x/y" # version = "2.4.0" [[constraint]] branch = "master" name = "github.com/czerwonk/bird_socket" [[constraint]] branch = "master" name = "github.com/czerwonk/testutils" [[constraint]] name = "github.com/prometheus/client_golang" version = "0.8.0" [[constraint]] branch = "master" name = "github.com/prometheus/common" bird_exporter-1.2.2/LICENSE000066400000000000000000000020601333773000500153700ustar00rootroot00000000000000MIT License Copyright (c) 2016 Daniel Czerwonk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bird_exporter-1.2.2/Makefile000066400000000000000000000031551333773000500160310ustar00rootroot00000000000000APP = bird_exporter VERSION=$(shell \ grep "version string =" main.go \ |awk -F'=' '{print $$2}' \ |sed -e "s/[^0-9.]//g" \ |sed -e "s/ //g") SHELL = /bin/bash GO = go DIR = $(shell pwd) NO_COLOR=\033[0m OK_COLOR=\033[32;01m ERROR_COLOR=\033[31;01m WARN_COLOR=\033[33;01m MAKE_COLOR=\033[33;01m%-20s\033[0m MAIN = github.com/czerwonk/bird_exporter SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/') PKGS = $(shell go list ./... | grep -v '/vendor/') .DEFAULT_GOAL := help .PHONY: help help: @echo -e "$(OK_COLOR)==== $(APP) [$(VERSION)] ====$(NO_COLOR)" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(MAKE_COLOR) : %s\n", $$1, $$2}' .PHONY: init init: ## Install requirements @echo -e "$(OK_COLOR)[$(APP)] Install requirements$(NO_COLOR)" @go get -u github.com/golang/dep/cmd/dep @go get -u github.com/golang/lint/golint .PHONY: deps deps: ## Update dependencies @echo -e "$(OK_COLOR)[$(APP)] Update dependencies$(NO_COLOR)" @dep ensure .PHONY: build build: ## Make binary @echo -e "$(OK_COLOR)[$(APP)] Build $(NO_COLOR)" @go build . .PHONY: test test: ## Launch unit tests @echo -e "$(OK_COLOR)[$(APP)] Launch unit tests $(NO_COLOR)" @$(foreach pkg,$(PKGS),go test $(pkg) || exit;) .PHONY: lint lint: ## Launch golint @$(foreach file,$(SRCS),golint $(file) || exit;) .PHONY: vet vet: ## Launch go vet @$(foreach file,$(SRCS),$(GO) vet $(file) || exit;) .PHONY: coverage coverage: ## Launch code coverage @$(foreach pkg,$(PKGS),$(GO) test -cover $(pkg) || exit;) # for goprojectile .PHONY: gopath gopath: @echo `pwd`:`pwd`/vendor bird_exporter-1.2.2/README.md000066400000000000000000000071211333773000500156450ustar00rootroot00000000000000# bird_exporter [![Build Status](https://travis-ci.org/czerwonk/bird_exporter.svg)](https://travis-ci.org/czerwonk/bird_exporter) [![Go Report Card](https://goreportcard.com/badge/github.com/czerwonk/bird_exporter)](https://goreportcard.com/report/github.com/czerwonk/bird_exporter) Metric exporter for bird routing daemon to use with Prometheus ## Remarks Since bird_exporter uses the bird unix sockets, bird has to be installed on the same mashine as bird_exporter. Also the user executing bird_exporter must have permission to access the bird socket files. ### Bird configuration To get meaningful uptime information bird has to be configured this way: ``` timeformat protocol iso long; ``` ## Important information for users of bird 2.0+ Version 2.0 of bird routing daemon does support IPv4 and IPv6 in one single daemon now. For further information see [here](https://gitlab.labs.nic.cz/labs/bird/wikis/transition-notes-to-bird-2). Since version 1.1 bird_exporter can be used with bird 2.0+ using the `-bird.v2` parameter. When using this parameter bird_exporter queries the same bird socket for IPv4 and IPv6. In this mode the IP protocol is determined by the channel information and parameters `-bird.ipv4`, `-bird.ipv6` and `-bird.socket6` are ignored. ## Metric formats In version 1.0 a new metric format was introduced. To prevent a breaking change the new format is optional and can be enabled by using the ```-format.new``` flag. The new format handles protocols more generic and allows a better query structure. Also it adheres more to the metric naming best practices. In both formats protocol specific metrics are prefixed with the protocol name (e.g. OSPF running metric). This is a short example of the different formats: ### old format ``` bgp4_session_prefix_count_import{name="bgp1"} 600000 bgp6_session_prefix_count_import{name="bgp1"} 50000 ospfv3_running{name="ospf1"} 1 ``` ### new format ``` bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="4"} 600000 bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="6"} 50000 bird_ospfv3_running{name="ospf1"} 1 ``` ### Default Port In version 0.7.1 the default port changed to 9324 since port 9200 is the default port of elasticsearch. The new port is now registered in the default port allocation list (https://github.com/prometheus/prometheus/wiki/Default-port-allocations) ### Sockets In version 0.8 communication to bird changed to sockets. The default socket path is ```/var/run/bird.ctl``` (for bird) and ```/var/run/bird6.ctl``` (for bird6). In case you are using different paths in your installation, the socket path can be specified by usind the ```-bird.socket``` (for bird) and ```-bird.socket6``` (for bird6) flag. ## Install ``` go get -u github.com/czerwonk/bird_exporter ``` ## Usage ``` bird_exporter -format.new=true ``` ## BIRD RS Dashboard this sample dashboard was created by [openbsod](https://github.com/openbsod). Thanks for contributing! https://grafana.com/dashboards/5259 ![alt text](https://github.com/czerwonk/bird_exporter/blob/master/grafana/img/bird_exporter.png) ## Features * BGP session state * OSPF neighbor/interface count * imported / exported / filtered prefix counts / route state changes (BGP, OSPF, Kernel, Static, Device, Direct) * protocol uptimes (BGP, OSPF) ## Third Party Components This software uses components of the following projects * Prometheus Go client library (https://github.com/prometheus/client_golang) ## License (c) Daniel Czerwonk, 2016. Licensed under [MIT](LICENSE) license. ## Prometheus see https://prometheus.io/ ## Bird routing daemon see http://bird.network.cz/ bird_exporter-1.2.2/bird_exporter.1.md000066400000000000000000000061251333773000500177220ustar00rootroot00000000000000--- date: 2018-06-20 footer: bird_exporter header: "bird_exporter's Manual" layout: page license: "Licensed under the MIT license" section: 1 title: BIRD_EXPORTER --- # NAME bird_exporter - A protocol state exporter for the BIRD routing daemon to use with Prometheus # SYNOPSIS **bird_exporter** [**OPTIONS**] # DESCRIPTION **bird_exporter** is a metric exporter for the BIRD routing daemon to use with Prometheus. Since **bird_exporter** uses the BIRD Unix socket(s), BIRD needs to be installed on the same machine as bird_exporter. The user executing bird_exporter must have read/write permission to access the BIRD Unix sockets. # OPTIONS **-bird.ipv4** Get protocols from bird (not compatible with **-bird.v2**) **-bird.ipv6** Get protocols from bird6 (not compatible with **-bird.v2**) **-bird.socket** */path/to/socket* Socket to communicate with bird routing daemon **-bird.socket6** */path/to/socket* Socket to communicate with bird6 routing daemon (not compatible with **-bird.v2**) **-bird.v2** BIRD major version >= 2.0 (multi channel protocols) **-format.new** New metric format (more convenient / generic) **-proto.bgp** Enables metrics for protocol BGP **-proto.direct** Enables metrics for protocol Direct **-proto.kernel** Enables metrics for protocol Kernel **-proto.ospf** Enables metrics for protocol OSPF **-proto.static** Enables metrics for protocol Static **-version** Print version information **-web.listen-address** *[address]:port* Address on which to expose metrics and web interface **-web.telemetry-path** *path* Path under which to expose metrics (default "/metrics") Version 2.0 of BIRD supports both IPv4 and IPv6 in a single daemon. Since version 1.1 of **bird_exporter**, it can be used with BIRD 2.0+ using the **-bird.v2** option. When using this option, **bird_exporter** queries the same socket for both IPv4 and IPv6. In this mode the IP protocol is determined by the channel information, and options **-bird.ipv4**, **-bird.ipv6** and **-bird.socket6** are ignored. # BIRD CONFIGURATION To get meaningful uptime information, BIRD needs to be configured to use ISO-format timestamps: ``` timeformat protocol iso long; ``` # METRIC FORMATS In version 1.0, a new metric format was introduced. To avoid backwards incompatibility, the new format is optional and can be enabled by using the **-format.new** option. The new format handles protocols more generically and allows for a better query structure. It also adheres more to the Prometheus metric naming best practices. In both formats protocol specific metrics are prefixed with the protocol name (e.g. OSPF running metric). ## OLD METRIC FORMAT EXAMPLE ``` bgp4_session_prefix_count_import{name="bgp1"} 600000 bgp6_session_prefix_count_import{name="bgp1"} 50000 ospfv3_running{name="ospf1"} 1 ``` ## NEW METRIC FORMAT EXAMPLE ``` bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="4"} 600000 bird_protocol_prefix_import_count{name="bgp1",proto="BGP",ip_version="6"} 50000 bird_ospfv3_running{name="ospf1"} 1 ``` # AUTHOR Daniel Czerwonk bird_exporter-1.2.2/client/000077500000000000000000000000001333773000500156435ustar00rootroot00000000000000bird_exporter-1.2.2/client/bird_client.go000066400000000000000000000034201333773000500204470ustar00rootroot00000000000000package client import ( "fmt" "github.com/czerwonk/bird_exporter/parser" "github.com/czerwonk/bird_exporter/protocol" "github.com/czerwonk/bird_socket" ) type BirdClient struct { Options *BirdClientOptions } type BirdClientOptions struct { BirdV2 bool BirdEnabled bool Bird6Enabled bool BirdSocket string Bird6Socket string } func (c *BirdClient) GetProtocols() ([]*protocol.Protocol, error) { ipVersions := make([]string, 0) if c.Options.BirdV2 { ipVersions = append(ipVersions, "") } else { if c.Options.BirdEnabled { ipVersions = append(ipVersions, "4") } if c.Options.Bird6Enabled { ipVersions = append(ipVersions, "6") } } return c.protocolsFromBird(ipVersions) } func (c *BirdClient) GetOspfAreas(protocol *protocol.Protocol) ([]*protocol.OspfArea, error) { sock := c.socketFor(protocol.IpVersion) b, err := birdsocket.Query(sock, fmt.Sprintf("show ospf %s", protocol.Name)) if err != nil { return nil, err } return parser.ParseOspf(b), nil } func (c *BirdClient) protocolsFromBird(ipVersions []string) ([]*protocol.Protocol, error) { protocols := make([]*protocol.Protocol, 0) for _, ipVersion := range ipVersions { sock := c.socketFor(ipVersion) s, err := c.protocolsFromSocket(sock, ipVersion) if err != nil { return nil, err } protocols = append(protocols, s...) } return protocols, nil } func (c *BirdClient) protocolsFromSocket(socketPath string, ipVersion string) ([]*protocol.Protocol, error) { b, err := birdsocket.Query(socketPath, "show protocols all") if err != nil { return nil, err } return parser.ParseProtocols(b, ipVersion), nil } func (c *BirdClient) socketFor(ipVersion string) string { if !c.Options.BirdV2 && ipVersion == "6" { return c.Options.Bird6Socket } return c.Options.BirdSocket } bird_exporter-1.2.2/client/client.go000066400000000000000000000005371333773000500174550ustar00rootroot00000000000000package client import "github.com/czerwonk/bird_exporter/protocol" type Client interface { // GetProtocols retrieves protocol information and statistics from bird GetProtocols() ([]*protocol.Protocol, error) // GetOspfArea retrieves OSPF specific information from bird GetOspfAreas(protocol *protocol.Protocol) ([]*protocol.OspfArea, error) } bird_exporter-1.2.2/examples/000077500000000000000000000000001333773000500162035ustar00rootroot00000000000000bird_exporter-1.2.2/examples/kubernetes/000077500000000000000000000000001333773000500203525ustar00rootroot00000000000000bird_exporter-1.2.2/examples/kubernetes/daemonset.yaml000066400000000000000000000016451333773000500232230ustar00rootroot00000000000000apiVersion: extensions/v1beta1 kind: DaemonSet metadata: name: bird-exporter namespace: kube-system labels: app: bird-exporter spec: updateStrategy: type: RollingUpdate template: metadata: labels: app: bird-exporter spec: tolerations: - key: node-role.kubernetes.io/master effect: NoSchedule containers: - name: bird-exporter image: bird_exporter:latest args: ["-format.new=true", "-bird.socket=/var/run/bird/bird.ctl"] resources: limits: cpu: 100m memory: 32Mi requests: cpu: 100m memory: 32Mi volumeMounts: - mountPath: /var/run/bird/ name: bird-socket readOnly: true ports: - containerPort: 9324 name: metrics volumes: - name: bird-socket hostPath: path: /var/run/bird/ bird_exporter-1.2.2/grafana/000077500000000000000000000000001333773000500157645ustar00rootroot00000000000000bird_exporter-1.2.2/grafana/BIRD-RS-1522082867844.json000066400000000000000000000152161333773000500212540ustar00rootroot00000000000000{ "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "4.6.3" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 1, "hideControls": true, "id": null, "links": [], "rows": [ { "collapse": false, "height": 509, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fill": 0, "id": 2, "legend": { "alignAsTable": true, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 1, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 12, "stack": false, "steppedLine": true, "targets": [ { "dateTimeType": "DATETIME", "expr": "bird_protocol_prefix_export_count{ip_version=\"4\",proto=\"BGP\"}", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "refId": "A", "round": "0s" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "PITER-IX prefixes exported", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "transparent": true, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": null, "show": false }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fill": 0, "id": 3, "legend": { "alignAsTable": true, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 12, "stack": false, "steppedLine": true, "targets": [ { "dateTimeType": "DATETIME", "expr": "bird_protocol_prefix_import_count{ip_version=\"4\",proto=\"BGP\"}", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "refId": "A", "round": "0s" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "PITER-IX prefixes imported", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "transparent": true, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "none", "label": "prefixes", "logBase": 1, "max": null, "min": null, "show": false }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6" } ], "schemaVersion": 14, "style": "dark", "tags": [], "templating": { "list": [] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "BIRD RS", "version": 15 }bird_exporter-1.2.2/grafana/img/000077500000000000000000000000001333773000500165405ustar00rootroot00000000000000bird_exporter-1.2.2/grafana/img/bird_exporter.png000066400000000000000000002737161333773000500221360ustar00rootroot00000000000000PNG  IHDRdK'sBIT|d IDATxwxTU!Ф"** ş ZV *Rl *`` X@p ("ғ@(!=LGd0ށ}?{9̝{9Ʊkq)..4M|>@4 Btp8pDEEp8HLL$11@ؚ5k8ꨣ~9t֬YG1)\\\Lnn.`R@i|>q HNN&66@""""""""""""""r؉(5M۷ )..6I  ??M(vӢE G$M9N?~?x^k ciUn̖$Ò@Mii)XV&|yn;z7SӪ e-ˢʊȟBٽalII eeeu*Pfǻ=>Y)?:!}v>}.mTiDlz;tg)gO87ͼ|>n7;v ==鈈>n߾sInœM~~ѽq#4!XI'N ]zn7{!999ih>_i]0{zi5epҝsS(OKIn e#disNZjenVc   BVږQ\}@[qUX+lss\@X^?O"$ry^ èR.O>49餓X|9;w&11>b6oqG>}ݻ7~UVqꩧ?7H=qԴqϞ=8*GVC8U6S^7%=q8޽N:uFF>|8ՋN;72rH.B=z4;v`Ȑ!t֍,FaztҥOElr;wj ZsYDDDDDDDDDDDRc [RRr[FIE eUs?,f1U[0qf T ~#tbȐ!,[8:(ΝKٹs'4mڔŋӽ{w4i… K駟(--O. ,]cxY{ vWК4iBjjjC7CDDDDDDDDDDD ӴiS\ZW}ƣB׎}<~QycKM¾fkQ+dM &#Ӛ1rپ@జs0{̝;sN֭[ǀXhݻw'99 ز2N>dΝ[ گ_?~eռ;8p F%##ŋr?OAAUWΣO>4mKvv6~!|MuNZD&~FҺ{-,f͚EQQt:q: bxM<-[v]xޖ‹/0~8~CsKOoÎnqݴhђ>ř}rg@VV~ })`Cn 䬳Φy@y>/êUtr$%_GTGiz+@ ϾUN?3˺$h߾m]urtc';iE[KXS{xQ@rJOy뭷 صkW8}wIII!99ز2N:$ΝK߾}ϯ"~iӦ M6L4r'OpH޽[xXvm?sիSL!33Ve]F^x7$..Ν;t:k=Çň9~ n뻋sj<|8qqqT[.+++x͚54oޜ3q=/_M&^H\~X[!,^zA>_?6++SbĈ^yj#"""""""""R?P'v 3}Mrsszhqn~^Q#GpӍ72mr(۷S;߸qO<P w9묳a'gS9k6L4[IΝ93 {r2C;^ɤ7:u*{cqj<0yW]}5mڴ=vz-%g}6۷K(++kYH#Z~A?<{EӮ |&盢Hvpqqd>_!oe 7|0W_1h }Q[z<Ҹ m{xٵkWݾ};;v#}7|wylذ!?L :3KAپ/` q;M4 @L@nؼygf/w}`0ȳ<^m+++xE{}_&Mvk\@Ё6]nO<~#Gq '*-N:oK;DDDDDDDDDϫ0vҥ| /ujٓ 4-JJ1}zx Ep]wiF:t8ū!ya,kM4Ȯ]qv7‹.G!%%n #yqV=gR 0=3䣏|={rWʡtɸ\.^/<}:v1jԍy...lem#%%bRRR7o.e}gVh׌/0glve$$$qF^5J4x_xɓ'/0c ^uUzgټy3t(@v;n<#G5^W%&&2tО` ³VO)oŎ;*mu(Ï #@6 t:+}F1cp{7u'bŊz]@=E cկ>=sq9BgW~Ϟ=̘>oG7S?c|v s=˫`! <_WDDDDDDDDDD+mٲg~$zwLOii)ǟpSq@׮]1y^z^ȍ7mcpt֭̊ZuJnڴC>X̠CUӦM=S'rA^۶m#//4[޶m>0={кuky1t8w7H0,T stԑ_~.8SILJVѵk7eeU333ݻO†A>r-,{&Ndul޼N>nWYL?E]5^wYkTw~cn\ӦҸqc=[32]w|7 O"TczdYVud_kc Uih} JQA mq8.(I&ҲeKj>aڵkǓO>ɉ'Ȗ-[Oڵ+~D^^O>$v˖-N:1cƌ Ӻ֤.EVi۶->hk 7++I&iӦ:vj;Ӿ}{_yGP!/%5zgբ_KLrAku; $ V{LiVq*EDDDDDDDDD㢋u,_wԍx(zԩ3k׮|o / W6ؑo_>iv/_ιG׮p xPe|瓒]h)fٲm/yW_~+Nz/ ^Քy£\[qWׂ\k.B[MұcGq߽@y7+_§|r@ 0* 5 "@v{K?:vݣy[o_18|WO۫Zi~ rss;1b3f 77Өzbȑ,XT.\Κ5k۷//#`6l| ]v=:YP(DNNNmغu+={,}劊o߾Ջ۷:ZvSϦM>}:cر 6)Sx)Sкuc4{mM=8gzc?3W$0`_ҹKj_}0>X<-[rۮ]aކkFnݸ可ǵ֬kv]m?MIIOOkn:f ?}{ysN~lN`8k/m m۲h^)i׮=bBp8uv-_/33iӦ:ܹ3Zb۶m JjՊƍO笳0 Lˢq`ڵAW^ɒ%1޼:/Iy5hѲ%cǎ%66|< Ą?_^^~#OM U{ep@hqKnͳ=_eKp?Eb9$$$oTGC<,[٨ A00A:sǪsw ~_G&3mP"#OAplT7l*?rssy*R۶mY`ݺu#55 0d֬YCqq1^x!QQQ0v P;wsN>A\GhҤ k׮qH?sŽ~Գi2cƌCH 4 rJ8v-YB 9QthZeY,[C=e˖\2d0]8Ojjjg<cѶm;NO֭]~>0wywu'^x<6Vu W۷l2f͚ѡC͛_L\\\Uuָ\:|>z-GVj?66aÆuV~:N=uQ'O̶mxʜvtLas,dM ?e o;Uc dliiL4O>4wy}D&M~geӟ'!!g}'|q3g>E#p=&7ŋy袋#06??-Y 6p\Ϟ}=rVZqwӳ_~I륨<;묳ILL_aa!۶m) YPP@۶m/?k.袋1 -[qgsm۶q_N :&Tؾڵ sWf͚d w~:ڵk9G.]۹5sf#akIIIxsd1~f!,Ѽxӣ1\׿TTag?[*G{_<\?wǃi^'c82vo۷o_ alСseРAdffiȻYf@iii <;wV;]qUKǕW^CX`tҲeK ..;ү_?,˪u:ԓ\5kسg:ubn ˸ql`4/ Q9,^&7{,fϞUV仕Uƫn[ˍ7 ?^b+V?5*WVVWmߞo?~ѳo&ofkұSp練0gЃLNٓz |kܸ1ƔvͣGs7Ϟ5ŋӫW/^x%~?_}WS ~1118~[[u?o.7t3 a͚Ǎk!Y*>R,(?cx\tŔN/ #Frz?oXΦ IDATGaØ199yhRm?էU'*nݺqUWsUWW 3g6|c{Վ5lذCAAAyx~jtVo 2vN Y[y~==Xf GuTDeiՆ~'xiӦӢEJa֭[ټy3_~e׏CB!rss?~uׯdԨQU>ޫsL8'|o^/;v?~K=k׮eذatЁ~?||Wuf8∈ʞ۵bњD,*|hg{ڵcw-C5rbb"f`,"""""""""bGRRo? 4tsw{rqgsg/gKgg_sf߾/0MN?}ϢCDGGǏW{GYl(--0Jb y4B]!Vo zsNNMj|g*HNN{^ur QGmVmW_?ҧOp[o1x`222saʔ)OW(@Yau^6 mMq33;un 0@uԩ0|ح[9mSeYG.Ċ1U].-[$WϏY}ҡ S>,Y7ҫWJaϛoIYYYYY0v˖-dffү_?ΝaΝ }*"""""""""""""ROjx,~Xe -z24ǤY xHLLѱ2ewє""""""""""""#֭[x<B٧+GG'ׇ8ȏJx(~gXqMڽa8l*:mڴaӦMд$>O 4ML$ "v8n<Ox[ƍ5*VDDDDDDDDDDDDD"dJNN&99" 0 #Ƭa\. ea&N?dJHH !!`NDDDDDDDDDDDDD俞 """""""""""""g@VDDDDDDDDDDDDD(' dEDDDDDDDDDDDDDYzb4mjFY\.`C7CDDO"""zwȁYHH94ƊwȁS +"""""""""""""ROȊ""""""""""""""DH=Q +"""""""""""""ROȊ""""""""""""""DH=Q +"""""""""""""ROȊ""""""""""""""DH=Q +"""""""""""""ROȊ""""""""""""""DH=Q +"""""""""""""ROȊa-66SђY"ZlY+iu,]Mj‹t Bl̫3_o ot(^9;Uy.w}OX{My^{mNzn¬Y믪mzv饌u##F~ --Y/tP&/{A۷{>qc)""""""RG dEDDDDDІ yIJJb!L:/ ={eٲe޽6sυddl w[Fx |!Z7xN=q3>v,̚9 B%K]5t!ps|eY%""""""RW dEDDDDDPQQ1+W`Ϟ=oiu ~:Nҥ_­cp1zY}^}Fs=eywyiÏXh7of {;˗/m۶L| fL^qFIs!11;v+dGΧ!55G1 2<TO=z1 ˖l2Ə-ˢSN76mXnm| dEDDDDDɜw~,b˖-VObbg;P( T&3#^|߀eΜoxxqu2d JJJ1<: `i5kƽ<6M9ꨣq VkӇǎc;b-K5wǎl.rx`26ns.ddfT8vMyj뭭^/SM#::Sv{񦛸2ztv//! |6.\B GRb"7<9Ȋ4c=˖PRR?֭[k)UFҶݻwsDŽdffK,#ILLd׸exw|ܳ݇6*S5zk뇜:v{ IBbcǎ#--*  RV EMa7o}N nӶ-W\_?9Ȋ46̳POff&^7c]6|ÿ_O=c nK/_mأG1U (7/7;hܤI5?\a͛0e gdTZm}|\ze,K6mXq׭[DŽz0^yu&?;ws7ܮj*xvNQ,OUM˧޶m[zDDDDDDQ +"""""Ҁ oʱKXlټ&#Gbԩ,x]V|U=831 O=4w,^\RoѲ%{r3C Aw"~LS ==.Szgʕw/YK=8&Mč7=wUvUWom_@VX|M-Zk׾@x~*<ڵ Yp=M6EDDDDD(HLHO ?ZiDd^^/r+}!?CS'&&2YpV3z-[v]̉'Ń'??:~W֭]˕C ?VzKSVtrĉdff0f̭Iy 5v۳nZʼWnv-_zj쇲26lXϵ]r1tU͑GvOzj_{nZ^/q xa Gc=ym6nqp5[ng ?O 0*,YSzIqu5NlYǏc7~+0h^u;v$ #Řne1d%ĠCjlc ;&y)\׿/ީ""""""rPM6?qEDDDDDDj?bѢy) J """"""{iTFC7ᰠ~P +"""""""""""""Ro4eH=ԬY3bccbS&MhܸqC7Cl'%%!6EEEѲeˆnp8h۶mC7C"Ԯ]nD ==ZhAtttC7ClJNN&11!6%%%iѢEC7Clr\nݺ!h߾}C7A"nfHZjETTTC7CD""""""""""""""DH=Q +"""""""""""""ROȊ""""""""""""""DH=qczԩ˙cB+v;bgb+vN,)β< ^6nY! i}@da { }na ˏAeU@P\*apؿ=8C!L3a6CdwnT2h*rKgδB8"x.gZ0i:@65%cmKv8Q.m,@ʼnIc71rХR.}bYU IK1 䟀"e2(NM b^?$[Ŕ0IBfBjxlfia9쇿0 aRLLaX6o b..Gа8-?!73XX8,0#Q.AN~,^NO(^_Z{c60I(GV9W %yʘ&ֽ4Ua0mܜfp89e{P$3BA,ݛ8;tl}Z^Gt=kVv}""""""""Ri {oDG­Ot/ ,hFZvǨ(F1)'!eNv0w8 T 1mc"/gr\XX}N{_yF`3h } ,P!./p{6`(Ai0;s2dAӏf$tW.1qut[k L{Hm|u3MhIiB1imAcrZC?+#7M8v}""""""""Ri <*=v@ ۴TȇǴ9pDt 5bK( flg_ΐ`ɲ,6- odeO`0ˬ{} 3dF0m'E0{'4qN5 _o 28llt=%s ?r'aؼʂD۝mmnk8 3y-ލ9 0" 0lNmmZ!<.؜ `(c1vg0tpڿ.Uc$;d{T(a"Kp+s`Wc>#৑o3Ձf+[uXR Y XJJȚ>`(a(.`:q8ϰ`}6?- R0VeIM ]t>g ,,f3c8,ѿ+V,]HUMO"!8vҼ#nOd!Vih2m)jXWɞ2 Ӊa8NٗNHH沺C#5''m:qil`ڜ6uGs3p OHM,QLvGBxh~G~lCX26GGr]e/- 2睝;P<0n,h ho2q V!́`ww}n6odďY Y?L33mZx"z'.)FaaڼNP] G ],°y1u9~H;AICO 9\y9lM=_:BQQ1^oiUs9"<פI,"/@]#&&9.' DNWLϦh*w$E0%w:Đ[v-|~3KF>ۿ %P,e!<.AF˳z/ {ao{ݘ49$KsJjNic zÀ$ m7&ˀ7h3WG(dt\.o_wk fZ>SlgX{!,_!FT2 yb Pjsiq#Zl>,х7lڂPNv>2M֓.z'| x?jBv=s;I%קUznc7{ҹ$]xgF2,VIsJ#K? njvOү}to3/ZUG@x`͑ f m;:WLS\?1)Jrخ+ss˲pF4N]&+e |<(+°eF!jONn|!`irX`sLŊh62xOTM&mE;Fts WEdMHt8lݲ#|0ා/H7TX>ϱ'aަ [lFIߎIitLao?eGǔs ^Ğ#msBjIYQ{t-?,]NDDDDDD$i:A|VS˸[# wm:A>|բMRG4|MoxO:jz}u>_@I" W c/,ƲMwixa~19XZ%69*}>) 1.L^A¢$'~c/d Ho8w#>Tb흛0x*A3*-Nlb\<*c8dm+ `sFM3d9/_>uwNbcHڞFavf9 /w}>J K0m!(,hF@A${kwarUuwzIg%@d0``. 333WtAYTd/#2#0 *6Y$^?DbBҟ~=:u={E9 IDAToYtI4)M0wd{3\-<6pUXAGn wVM(MۑVI&A4&^ޫa&&*2!bl8?Q=/xj@[-꒟N;#ve) hU秴h΀1؇CO/k5ܛE e̥DcTWOr({joE4ChȟFͪЇl~ul MA3 -&P|ٿpK xl [Cx#X>p~@VB{m%@ӑX>@bdg)cd[*~y]tò{s\ض|j@ep:C6EՄ!tዺ@&+ޮR` f&IaJC8s=ԫ%}/a_z|f0V0 &7:Go, hm[|(.hc<׏*jEW,z-(de:l/a\_7YQ <@DJ6,TV0 h,Fc0(cXz ڨA~=*j1ZP6 Ɏ7 |_a$YɽSh@s<z4}|߲D,j*!B5 8?{>Gy6d"=kFh>>38ۗa8!;ҩtPw$ߏbaNDDDDDD4Z/?|3 EGGHF7"'˿#\dzW7j?N*E!~nu/ng&13vd#W霱Q^q"sDiB XYl fTfE#È. « x2Lrӆ a ;8-R:6-o!݄.'ke36"60O(ZÃ3YGnKit-!?0o'FH_z̨|Fe a ZѤpbغѮȗTp[%n6LѶŒ @=n|P@-V,Ö/-i"}B nS걩֥!(Nu#һ@NZAITǠ(mX156h%CGVܿѠ#ȏ6.#LJm˂mbnyoڨc`|y>VpbhWEnĞeς i܊X/ K0V] ۔ ^I'[=蝢t|iU7u7oY^DDDD3̜ ,ON7 NfQm!Bcz!Pb\wa VSi=ɿOQ]n : _wp  a]"'oD[a͕ 6g#s;:*PnFElkovA0NMB̈8])!m,:Etej"R|/ :F#("n iK^'ZYS~kM YP \qSA-/[D֒4?۲@mmpHYXXC&&OSڮ+z3Ma`GʒCMlxMM&rY(z+cmApd Lua4]eaW2u[ZW|<$Wk0\I `*(DPkEWLۯVa 5 5 ~F3C ܫYi:IDuShދ#(ڭxh.w+W`[ @TT0:T܀_R'pM_?[ZvRv/ ^yf=dv9sPVjV5'Yn݇? "\i=zdH߬>KxO |]avІp =/ R8 uy:+ ~vI93)tdm8B{:1#`]L)4hw"p>|Zd{a_@`.\5Hk& } <*uWXW;pJ}k+տ=Nzb!|3TyjT> -@ivG!6q栂!Dž"w/BY۩\' h箄E43TU1E%"zsh@6 CDnB/|&NmUHFVѐǶ%Mۭ_[T*P!h !\d2h4ұ)p*΀y"o[S ipK6M]$ #(hu<K=r0qsQH.,#b,w<ʱ)֥0?Ҳfo?n(!YHg~@4]Qh\.Q hK8kP t !6Phož]xlʿQ-@ChjvS<kC.Dy!EU A ׮$HKh 'Y[9̐}€s[p[0, xZ ШO^D1:LD9KC+\x1ׂn~І0Qޚ!bۨ T0B =hٮ7`\Ӆn*EQ "hq2qX Y`{Ye_a HjϰBDaC'a>6M reP:ii4h4&u0$+I4 `׬?|2Ҷov )Hgp|[iSkZwބJ^TX Xb NZMQYذM|Ѫ WCQfHv+l{ xyctNۅj]x]mdV3#H%Q!܆Pi04z ZQ&Qh7=Db|/!ֽ8>LBdr6zфlˢ>\vs`sC—yLYХ3/=/ X,Qզ<-TM! tX 礴Xx,BTVJn[!ۂ0=4 L#CJ> ʒP觮R86t*T޾> mܤTL|V7 aClrެ5eqz@,nӥnyȏ8my fJSDd"Q)^=(,IziuέD5ͮ, 8.itl[|~|y?s8hKJ>(6="vll6<3ћXAk;b1GcN"Q:VMB>|7gtvo(fx[0E&WyXyQDnߥ¼BD]hXtittiPOy^ӕ:ulX tFg6+ӕn&, w:mwmokqȯ?ɘׅ6G)ICs$׋h:#Nx.tS6k5pʲM oFpL:XJ,BAഡ۲L{?E%"zszzz:cHG~Tb qt{ӭS AK04h`^:-^}M>=="|Jsm(1T¼6lG5u\Ƽ|vAœ|B\RtJٚB_^ ]ױP@p:SבFw@Naρ5ccHX8Xa߱tcZSYp4l<6qB– uxu&4Y[.Ѩ8]/UyA}z{}_Z'T(t]<}myV Px1\VNm=N ]nL~ K._AҐh-ZRsuX1zB LvHZL=ЃCvQ-D `Z&<35n[66:dY4e݆!|r ϰB$ ])!4ivHJ{1:±֥h]nCWx^V='}@uXb}7}9Y W[+ w-?d]Ӭ^} C"cW4M~Z1Dgu]ЮVoQ#OQsnת$e=0PܨTanW)n+.Q( ݪT\"(VB7 З' 5$Ӳwؚ0Xa9VE4&_zTl -+ԐĴLu$V% y>.W@S{Lf'X!N:H5`ЩK}}Vh5Pi_:QIcjl60 Q,)B4MޕaH$bUK4Tm70 ~PXn9s%niJ햊FG"1A i"!yH DPl+BWD̶(fMje4MX."b1,։J:]Ph4ml ϯJ{tw\ UxidDj|`NRؿ42a(4q`@FNIk.Q]i?v./۲YuhkH^* ڭ8mһqv][<ZmM,"' MlPaKa_'ίR>J- % YbEbDks-kXB 1g[EW|d]lի+.離ǦZ*mr@YBF}@uXx鸼5d\f>J{Gǐ˯D\c >il`l 9wVIU $ⲗP!jcȩw /{0 ٫h4X^ z=Q9aC)RڭP+(^"&ZJK7ubȘ)W)։osR'{zН-* M,Bn} ӶyD=4 zm9~#*,-|NlBX.:NW.nx.~z͗DDo. uP'}|{?].8FYf ŏ<[ݝy,{pz#¥vp}'b`{(ZѤ|oJ^SYNPjUEՄn@4x|lRNA" u;. [v& "]f0 4Jq h¥U^fXBװSN( S\j٩a'&簾iL hQ>PGDf*SaVvl" r]č-˨Z'N;!߾gZ@zZ'mprRSV[Cwu3XAUZG^?ahl7&tٲs+مR$>6UۂmɞpOa8VX IDAT?~?u'OmoA|ܹʫp5W=DDDDDSXl۸ࡇWA/td2x)+âtR;p'%w g@2dYMĞ <σ2NuBP1S: /g@k#H< >V9T͝;BN3ب\師gqnߛ{/E\,/'%""""nmб7=N<0,fEꎃpdWAD`Rܔleܲ_,0i@ͫu$HzU^H~k=dy4}b2aƆ($`&x(DjaMqtDc8 uiCπT&v(]3l:#;`\r%u###W;v/_+W'> zuk֬_}ߍSN9aRK.A>G&׾5DQq~[&^~e~袋d}v/cݺu/~z7kb1|泟Ņ\n ?i?<5|%""""-hNVq8ʈ9s^S[ {q(]^GNa?  u"uwZW"Do{6@ },|lYFoQFێhk*zo?~~:>pꩧ⬳+Vo\s5XhFGGlgqN9j5y8sGy$ytxPв,U~x˖-ýދ l^xv>p 0 x;pWaw3ފR(>w4 ?û$_d64T;dXqPl4j:XVP]M4.xܹ8rɒdhj;_H }h|i\{[?iN9>h4Xd Rd<@k }_ZK/C9s |e-Gu$߹V|l6ߎUV OO[ dh4ɤ T/0?m8&^R۶>DM8nK%dqQ T v cJ/fuF /QT_URT_:wN,B,P?zr]D-KWۻyz~@{kt*\Gy*!&G@UөS>,6uo{j"&e2,@(xla:DB|{hP((I۾' rɤ8H_/H`TBR**6Rs C?]A c@tvف̟?6l0 j`Ɩ?R) oׇK;WZ|3BZQG{n\~Q#;ư|r\pJ\2|ߍG0MW_}5lߝw9ƗA>裏"Ps;\e˖<G陾&ߏjF1E!l60 Q,($L&p-"r($:.\kNwQHŋf͚. -\:(gr=8'NEةttiP(Ly^-N1!NcnwlӕN4/4?g6lذlK'kvcL^3=y0:2W!! 1m͛|>€1ڹf)^b6!!d&qu^p,@hth&e:a yʳhzamVbN-CuD?>R3DDDDDDDDDDDDDإ^ l1 b{tv@;5MDDDDDDDDDDDDDDSKьI&az.JKozJeDDDDDDDDDDDDDDSY""""""""""""")€,a@h0 KDDDDDDDDDDDD$d4K0 KDDDDDDDDDDDDl7ۿ8.䒝q 'lwry41߽mmsD'>cݺurrypYg_m};߉|#[{o;.O{L% DQzio|z7i<0~o'/gnCSH^{_ipq!N<gy&Q.0 gy&>wm~{Ҥip {}Rc0v;퓟ߎr ]{3~:i~K Y""""""""""""10; kvuu]~)4 { *0>w{4 HRZJ,J7ْo}+,\M45z{0:2gyfۖ.ž~y3 o !Bhqe 493 aӅ( q|q^-vsk3dk0͉D6a[&v0 O d0}țA%p!_VwĻޅ S9|1}4̙eIX7>>4 9|%K^{wq8 Ӄk| h6[}< x98sgQO*d9Njm*a8ݺ ,+uӹK D/TYCYl1,0 ̟-K`TG6i]@xl%헷ڍcCZ'61З-4-X,]4ї y,#J0'˱RL\u}q}lhoo0a:^yuX4 oF ]( 0LC|Kut ixy( (]*=ӠPo'y۶d yx!>*8)[JuEAZC)cST"*~~Zq^s]%0+N4"a;uC H";)ߞ\m&BTd@*J """"""o"X5 6  իNC4E¡ݴ]]]n\w߿ܹsʷn4-]+3[~o?2||泟e^:;m\\T=hD~#zyY`cj .ErPCf6X,3 a\_|؂6x%Il5bn"T@@XGO,MCP ~ɲѐN1'k#'h͔ ju }?㴏Ǖ7:LRk! I(ϓjOؐoTBtXi;%5ЛI_o<"Ώh)&:vT]z'q 7_1dzktN:; ̧͒rzYQI5s#H:E!]M4wv)hl[4 l?{cӪPou3MӀ+GDDDDDDD4t< KDDD;Pa'%ڙj U!TgDK8=+=~q~F I~_bLaV:0 KDDDD4˅!0RnwnhS$(@7ؓkdy),55>֮DDDDDDD3dh Jm^Å*N>/NE*&R]1QD"c)Y:X?XĂݢ4 O<=:Y"""""\/~/aou ˢ}h7 _ʼxزs5Q(Vd1Wj-`8_eY̏ГMҘ~,|f""""""y%"""""z)w&33bVّm,xְj3ٿHy>%""""M*Io[ôgd0Br:!䪬5!,EDDDDٰ,C!:=v)dn; 'OMv@ҙD;d$|?kRӛFK>,J@;1 lc~KۢZmz,|P8>l[(ԉzuOa&*)k3cSK `V9GsRlF"h,S_Єo]SK=% [L@wwED &>@uqa+Z:a ˩RXL 6s0 tuu!Jr/Z{[BRNT> ]l J|_Wqz]^Rfm/DrNjP,7$p0]X:ӏ<@LJipJv1 KDDDDDD˛V3Br}zn-U^&@ҩ,2P?NI*i6t9tN(R4Ua}ަikvV)J},\k׮5mI{ܹgz^Zݱ:3FϗnV]!ىY""""""KMTjˏ& LB>E!hԇctLӄmvuR-Y(hPju.0Kn.`3wh"""""""""""",㪫bǟ{oD"xL*?d4 |SW_}}{/E\,/瞛T\wᤓN%\|>xcʕX,| pÍ74M_C<&/gьai@4v_\ua_{qn>sN=T<#W>'|2Lm砃wߍO?_O~r˿XvN=TcѢE;LswRଳʕ+w41= z(Jwp|3i~wI!KDDDDDDDDDDDD3}9{'=2^hvI/pymaY~z<3h 6 mXz5W^Aoo/r g4 ,YT oϽwļ12nڴ ˖-}vy3 KDDDDDDDDDDDD3D`h#. \iVke9c1w\qkvi$A;wC}ނ#:ׇz+>uh6[}oǪUp_'N'Ì,lW<5<I5M!:(+5ssY2sf9GԺuk%&&֬Un$I]vUyyJ|l{s^I#ji4c/?sN:)''z޽չsg͛7f٧~s/C9d%꥽{,4c%bE84d4qD\.IY_kҤI>oMCqmvm8-YTgZZR;wԩ44*)))7@qc ,/U,VNԳgOIҞ={PIj+GZL~Ȑ\~ȐK:xPk_&N973ddhP>Zr$ԩS5k,IgΞ=[;wիo(##C˖- Jhk֬Vrr^mۡ! f0 c<233կ_e۶mӶm.]h-ZSA u=ĉ|! @h@А! BC ##}_VVScl6yZ3C, YeDGG\hfXlZׅ)uLSs:un׿=ZZ׭Xnp~U]{>! 0xj]^ZZ?^s=5-ͫiҥ{RQQevVXC,jaN,Fq.z}O>>c$=*W93vln pgƍRdd_+Xii):~:w)SEqׯ#>)>^999 v)0CSEEEKi##}_^^S&S U|>hi:AC, Y4a~:! b v>6?9/Pf@А! BC, Y\պuf*>,,Lk׮ P5׺uk]a㦚ZhnHL) Y̙@ݻwWMI*((ln׌ԙZڷwO9ix$I>t 8Pt뭷j…ZxRSSp8Y*|t:ȑz%=]|SN[^4FiҥϗRwt12 C7g%/[Rhz<Bw͑Tjݻ:wy,O;PnNv_fgg+99Yzi=>! XI2 \eeeŮZR;wԩ44*)))7,@anPBF~oZ*Iڳg_:\YfCC\ҌZ0qϹ! &ùFyzjвe4uT͚5Kg֡CF45kV+99Ys6lАU㊎$-ZH-Ym6m۶1 hX{}'C@А! BC͆ax<TGC ##}_YYsΙcZ} ֺ 4d|xqYqY Y 4dpU3,5`Fп=ZZ׭Xnp~U]{>! Zll-]X.]ػ.ggr :p4dl^)..}kA pgƍRdd_,j.aôjJbݺah-~KCMQU_̖Dp3ggzcsrrԩs']׳_rR+U+Uo#9:%眭ZRxDJ}ԭ{7䧷Hbbc5w|M?^eeeŮ}=޽[ONGħ̐@ѐf$Y /95n8l MӜW5rĈyy9b% 2Ѧ ȑ>! &#Kŕ{ `r<|~y]>mCɘ1(y4)(yCZ?2衋OnϹ! BCW7'! 0]p 4d @l. Чns^.̐! BC, Y@iڵ.>0~[u6nDѐLx<.^޽nJRAAevfԒ7Ծ{}ii4p?9[nEӧOWtt "ǣB͜9Syyy$ŢI&)!!AztIСCշo_Y,;vLiө'FgՂ^30 M6mܨ۷UWW+99YwiԨQ5jRRRn:=5 zw5l0M>]=\ͺ+VSJJڴiN:&#GҥK/61cd6oK^fɰۤgs +<*-ߡ yfIR^k.K>3=35̔$effnE*..֐!CtתT\s"##V#;PnNv_fgg+99Yzi=>! &㣗;]8ˣAӾվc~[RRRsf0:csarݺվ}{5J4o<Mm&IӒK*++(vʕڹsN) Y &!XIZ mհXvܩ>}Eo]{YߺukCԽ{wD:} fCC\ҌZ0qϹ! &SRo\IK'sRt͟?_*,,̙3ko߾] Rp8j!]gkbH}hk֬Vrr^mۡ! &O}9= pѲ>L}%zkN~~RRRR#kCk]~O/>qsqb 4d @h@А@a1'ق]pAtdO].Ξ=kjb9/_XXrfb!! fËvh"h@А! p! 4PXXRg vAhk]bջ55Wu,iiu+--ՈojO]tQb^***lnӓ+tr| \BCC…ztO>>cvء?ִLs5zXGt:6k>^+ͦT3z~4y<`Dczq,rzqah'IFyidqUS]ᗔ?nJ<<$t*))I߸Q _]N󢱽z+Z0>&<KIQeeϘoQvKcÆiʕ*((0ah-~KCMFSoRdzTy9v:wǏ׺P[nuݙ3gt;߷O,ls==wt]Ϟ:Ϲ9,4c%ɰXeuKN˥W:VT\! &2?AqrUKNۭyso߾$ͦlMÜW5rĈyy9bjx<4a֥Oy:>+rb 4d @h@؂]pA~aO=OPua, Y4aZ!h@А! BC-[hŊzjĈ 3hРW^r6LWֻᆱ+V(>>VX%K())Ikךq0~[u6nD| 43sѨQ.Z#Hl6VX 9rm֖-[U޽{+99Y?\.T\\|IMzuwK<O]mwﮤTPPp8ݮ37o^r2CW5ŢT9=ׯ5p P۶mr~[:uY?yd򗿔$UUU^ӤIjy뭷j…Zxq-<%I*((C{UXXl6[4i^u-]T_wR3t:ȑz%=]v804eʳڴqos^n:uJZjbcck-[Liii SO=U<77WLvh{yw(7'W;zcյ[W%xݮ'?+VԌhh抋U\\,ŢվC[Uka7I}v|snh2 !Ro呻gO Ull>#:uָ? .n◼4dd}ZFv{kg5BUh߯cbQuwڸa%q9991)I쩃!XIX ׺"Nj12n}_s._{K=f}GCMFəxt,"ՠao~?YG3>mڰAOsnY &㫅u70$9qhUQY)ah=ڼ9c>XN7&%;RF^! Y럟^o#C@А! BC,- czLAQ 97! BC, YmiZ:uBBB.۰qSp-\X7$&,0Lx<^3gWиJJdyzlh~o5#uSnOfpZO>ڴihZtnV [oh 4HÇwQJJƎz~Bqqq7z͛3, .{xxqJU-ouu\.\V޽չsg͛7&O?[n4{h؟bĉOiz/Vrr{={|KCMƽoUx\z.>~b>ׯf.ۭ H:ۯZ&MK.n_ۯVΝ;5u4 MJJJ|gi4G iJjUXl򆄄v+66V'OK٣dćJVke4]-[a7ӟׄ} Y4Ǐ*szJUr:oy{'NHx y<ӧ:uf͚%|CoڹsV^7xCZlݘ VyEvک7\v̚5{W6|unh26 J={(--M/v)IJNN?g}V۶mӶm.h"-Z˅I[l-[ꍻ]8qϹ9d1 )**JEEE5{ӧO"4̐$-X@ǏWiil6K/,\h߯aÆ 43,,@6vMF /,u93d @h@А! BC,%өe˗ #.4Y^_&p\bջ55Wu,Ë1ZZ |=x=J[?:vVXCh4y:%$hܹ:|>GgЎ;~:bљ< . T\\bY,wuتZc иO꓌ }KsӐ@aYZo\UU**k_i#;s ]ӣL?Z~yQMyIRTT~=9~n~?ݧM7΅ฮgO Ull>#:uָ? .n◼4dd]vUyyJ݀l $ۿ_7&%}5֡鋊CY~?YG3>mڰAOsnfɘ+AIԩ^!׭]i˖-Zr^5KSN$ϟo J0'11Q<8Pݣ͛3.;tcR.ed|un͜)~Ѳ-[˖\/_^2w>~=2衋OnϹ9d1 Y 4dЬ^ < `\_X6\n1EŲZ> ZX-V=25nfi܇bXdx11m0]m>W3xdٽ(riY0 CVKV9wsծ\52?Kط[^Ǚafj6-\o/\)hhXuCBO*t8M+(UxHqajz6`˖-_ӹJ+Jms݊pFW媖d#ReU\e^> 5/2*JG-e5EpZafK>)(-4IReu&.!3,LCa9 W\^"ɆMJv|iEB5b*}xLD3p(**JNӫ&ynzTtU*ӹ6 u0jU\\"r{7mޗv77~*Wka]v w0YaTM UJӹsM۶0e3?/yf_Av\SߧUNHl2ko_nl&~&]94Օ 1:xL('**J%%ne{6o?{~{\#?U#""dZUТ .h}>{x74hB-,i͛-\ͦV11rT;{ ا7Gvd+zu{>2OצZUmό>ͥOtڑ#'?}ǭ'"Ҩ Y#TMl:Ĵ7=x udzܩ3"|싼T~Q$Y~%X hiz\aY"JF6}۲VVLsHKMH;Wrk.sնe[s<I:[|N1&'~6"odtK CrXCL7K /8wʫD u໳)-Ë/ K 1UCD E{wYEڶjk5۫~@/W6߿}#<<\= Mqb"cLk^UUbd}OcY&*t>ox>[tN1->ߞ9Mk4TVLl:Ę~y%ؕOs0O#cMjá 3?wgSϓqZQYZ|yFRnA dw8dH4܂33Bj}o>{{*ݮ&w oiN~N޼RwAIL~wm#>bQHh>(\>ż٧ 74r\*bM9Njƫ>MmONNN1L74UjMZ69@W#66y ЦMԻ2VZܹs.&DDDt̙3.&(&&FYYY.&X,%$$.^ҥ;2`RBBT]|kNΝSyyyK 111Ri%ͦg-[*;;;إͦvɓ.&uUG v0)>>^9992.W GDÜ;wN.-[9pزeKK˗/עEԢE`j筰0]6eE۶4} e-ZWM:M!!sM5õpbݐSF=d1ixݮ37o^rӐU/--M/։'$I'OW_}>HCU߾}eXt1O>zG$I;vԮ]Z0-X@TNtرC5\ &h֭[oUC QaafΜ<饗^ґ#Gt-_԰aô~Sv]gVJJ'H?Piʕڲe,XG}r:z~IOQ6i$%$$(44T/8HE:uRǎ e)Sզ}6sӐ@b1[lQ`YV|z%I+Vŋ%IsUNW_髯o%K\޽{k̙:|:?^)))z_JӦMS^^ZjQFiȐ!*..mݦ~Zƍ$%''kݺuzWnIJOOף>{N?***>}hǎsmiz%jիW/M>]ٳMJ4n8eff[nJKKYwIKӇ|Pc0 mޜᗼCMaY-׼IR߾}k.0 2DsQZZEFF֌3f֬YKÇK=֭[ךW^ڵk%I}zU@7ohǎ$| K[ Ј#NECG3g':_lum! !%j$'&Jy^C1 qq zn*\Fo Y"4d DhM x>4d DV'Ծ}6L))PaAA$遇3O?7\ۍ IDATn4=7gMxxDS@ vk/k̝tm#urMػѼ+f$)&&Fx@[lQO+tI'7|zKO9E_~vJJKpi:3t񊊊Y4~8XK*UUUn+S'umktǨ;7_=7 Y-6@@ԻwopzIIYg{GK.Dw%߯kN"SO:K=zЛoA3:{w v+??qffӧ~"rbN9o=([dHw\ONPMͦ <ؘ%&%nh^h9~__\>}Nl7_]k׮՝5v3dj4+I]Τg#9NEEEI6|z;6ܿ1c4cto4CIJNlwrMZ`zkss,ZbeFfv\N{y4e$;n$&i:cmV]V6lЌ+iӦѣ. aiuZd!cy-[3P`zn[zzzt4C'55U@@VSQQQJKKSAAթݮm߾T`B.]m6ӀA999*((h2dff\VxrN$%%tjϞ=VBSNS˳:ԵkWmݺ4`PvvxNeeeTuuuV*%'&ÿoewƑd1`DА! !BCB,*\ Tݪzn8CB, Y"4d Dhh:tԃ>Ygq+**q .j:..NO=N#i!4c S8Uzto&N;z zgav =zPnn\.&LRw}ݻ+""BSNհaÔb=Z@@~ "ݮݻwE]~iڴiZ|^}F=쳺cIw߭?z->2mCee*++eչsguꔣ͛_htfӽާE j򯂞3djDJ9쿸/ֈ#4l0[;vl}}ћoaÆ?߯\ :TNS't5h |ͺ.L\.FW_}q.IP駟>zךēztcj2ܹq~l6,Y"r,ZOnE;gպwͪ$}{+..ֶm$IyyyZv$IJKKSll:(͜9!>;h={6:?O͙3GozjֶȾa馿(ݮѣ/oqиBW=~k_.yihN3Vv:$$4ڐl=T~5k讻:Z 38Cׯ7n:W EGGKw _jܻᆱgϞZbEP44m6 8M7nlr67PAh5>%={r4aCeee7n&M$i_oԩZj^}U͝;W/ /丯JZt~㑬WrakUZp!c^{UW^-XmJFF*++U]]mu*0 55U@@Vf CTTTPP`u*0n+''G۷o:ХKC/GNN zNdffIKK:$ө={X VJJ [3NS˳:ԵkWmݺ4`Pvvt*Z,t\˥zC1 QF7{l͚5go◅K@vviƶ1\ĉթS'×JFGCرcN!% D8CFrbbۨV}}X=7\.Ws, Y"4d Dh@А@b߿u=޽{Keo1wݭILգghThEFF4__i:6.NSc?>iSԮ}{@TW55Q :gMػѼKo4dpuȑVҎdەӹ:fekmlcԝhb盯_ukJ~wY,p\6W~~~z͒ͦO?E YNRc/6:mY) icWBBzm;yP\bu>k Y>5L'vw~,jw_oE~Ŷ+ԧωkڵ3wF6BnwPq,Z4c%+#d}7ԩS'{-mX')9Y6۾sm6_?C-Xܜ! VcR*'k_T=#zgZ|۰N=tW^M֯Ӓ%oW:省ZsӐ@qߟ:m߶M}i}/;A>\B, Y"4dжlc:` +mx<1y~3d Dh@А! !BCBiu@kd{|OSOi ?Ú?֭[wz\r:d۵lٗzv\]|%KDGkǎ5]?lz-׫MxaUUU3d)--a̘1%C3uMg;4Եtz7tC-p>3eff_5|-ymڼy:r! O<~z)=3z/\ӴitW2}q|^$wրL}*--ЀSNz5W Y%%_lTlH󈊊/I:s ݟAo 7ܠ|]vezr4rH!WG}_TJJc.B}ч3kر;Y}ھ}{P9h5{[R;vӐGwyCݾƉ'駟ֲe˚}Ϟ=uQGi̙ }g!ʕp͞3G*-)nn/[n$kN#n]e<|!U*'΁,Z4c%aw(#%E@@>ۊ4o<5fi͚5M^5}c __}5ڵkWڰ~$I?L'|B*,,@pKUVkWlN:I~m͟?_;wVݛuuԷo_egg7$9^:sՒ%n4x^y商 u14|Lnڵ+?y9C%_6l6Ν;kĉh޼yͺLƍӤI:uVZW_}Usŋ /jNuzTQQ!IݻڲesدZ}N>^111*)):4X a.]h۶mVrrrTPP ku*0 33S媭:&#eu*0 ))INS{:Z p:T^^թ]j֭VU\\,cu*0(++K:6[񜹊"4d Dh@А! !BCB, Y"4d Dh@А! !BCB, Y"pNt. yEli85ǝ(wU|sv/_קhpW~9 ŵz%)hoZ3s].ǛzGFG*22ZI1r4fm1`fUoӡuz x~S6S\ovRҕi76ז8ͦvJw ?>׸p;5=M#x ׸\o3$퍍N>籦9 O_uunCqzGEPUjזm1P\\:Ą4Rۮw4v]IIʰZ$4?׾}2f?43HNMў9|sr83NNIXaޞ__o#-==3Idn8..-8qUrVuU8/ؓ|tlj \)w_$㓫TUaDԩ&Pk|qxRTiމժv'؋lTLj;>JUl\tLj _d%Vm'Vc 6JՕƏ3#'PLdT;!J{h1Ww4&F5U?%0|`"UWodLhDUm1gٹS2\%׸:{cެi5MdT 長̚Jn9iZ.5XZ5&_Y5X연1Ś`iƚ`5u<m~ƛiƚ@v_#x!D^UUjkk +:sp8amvUSc qqq1~B p8|, Y"4d Dh@А! !BCB;$ښTM4IfjIPÅGkNW_}ο|ڥݻwv[;zxvޭpV~M_>,r$ Eݲ4zX)C֭[}:tl6FSw.EFF~nyԭXuItXO(w> PԍItXٺ굌ItXG(o. PԎItXTDDD2‘ium[Ghҥo֮] h麱> uk굌IxtXGKm?%ҵcm-]7&@%CzXlln$VNSYYYSO?#IٳV,_.Irݪ(/WFFFetjޑʪ0͑&Iճ>0ɑ֫W/[_(B떟:ct晿ӦMûCGuk*uItXOKNb}-]7'uc}fkh麱6 ĺ$\ZvM£h;8C6L/@7NzVtt|>4edIRBb/UJJJjǑ&fӭ#Fݻ52eUpD nYYYj׮ƎK ל9ڕ#JϓK?\{5vvӜ5u~MLl6S7' nnEeOLw;B'.V0cmb`h;hȆIuuƌ?_|I%%%zڲe$i޽\ !>A.zM;z>#mذ!۠db)- {G`V[[;vj '*''G;w N[SNzJMMդS4T;C9uk kS;S7' nOܺ5Z:P#kS;& nMK[֦zt:uJVZ$)];7y;hvƌK>2#Ѻeff /3ta/yo.mÆJOOoQ]]%<޶l"I*++SEEDVSuk hXFku0Z7'5U^XFڤu0Z7%ڱ6i\ᶿ 5Q}OT<! 7|#Iڳgf͞nݺԐ]f$)--Mc-oCv?WeVXxxo^M{ʕ+4r6yz .P6M_|L_~FuMwsNg}ڴi!hY4dbiii: }͓ߟ3P>O[6onVLΝ3Oƿޤ??49kt 7'.$vHmذAfofӤS$I#oM7~ŗ^TZzz衛oIOר;s55cv?ݢB]yUzᇴu{qڙYc[5kL˺37WS-ӽܭ3w駟hڵ<555}/In{lvqoKvM MWRRcF+//^dnoJLL 9vٗ_ꭷޔ$i9:tRIRQa}zc՘ѣi&mڴI_}RܹS]trr:7Gii9ކc;Uvv=^zU[[pL7oV4~} gUw>( ru`-7kY[Q<Ԙϣm- _zuEOe,IZ|cd1ⶆ>?UV^uQQ$)%560C IҤɓ}東ZVV{O_/.֭[M6iѺskzU\\|~Wii$Np:_xa.Rg44d~s}MKw]v4 !7u<*VHqt̔$Ϗ'=Ck.]$I32IOO?dECڀĄ|rwqЙz晧umkhՇtqbbF1JVZ%IzE16}*))9(uɥ*??_7WUTTo6mܨkE55[QlT7\ʷ)Cwvjuꮻk>vݫ.]jƍQee^!2Q[[͛԰nT]]NV}mٱM))-w)Y}]|JJJtTSSx]tџB٭N_}6Yg7xC[l֝86ovIҜٳn5s^ƌh̒%K4zt:t]cy)@ ѣs;ZϜkIci zzt#;Fr@@_~<=6];wԜٳ? M@ Qwܡ[{ȑwhU{^nw0Q^v!讱cT__'?Ys#$ h-==phX .:VXFnPLLLBd! h16٬NUxh@pb)\ gZ(##CVRSSbu0(>>^ڵ: ;Z :ꨣN&u`BNNNiLEGG[ JKKSbbi$Y Vffi өN:YLڵ)lEDDXLRTTi"4d Dh@А! !BCBb"ctVldḪ*GULd@ rUd 4?f?w[qq܊2y0; USW(vT).Dj񱱱 rWe{OC^QFSZq&83y~C2~3Zh9Nm竸X2P5TeM8'|?lVM}8pxam1zq ٛ߱< O%\>q+7g{ 7ۼ>~F=&\<6>ތ+` 9q::qxÅd1ja(&&2VU^Yn(.f7ހ/w+%.h%3<_YeRR ye(I*(5!p\1[WII㌰lӮݻLevK3|z y" $s9Ԯ w $$$Bo𨬮TBlI*[tq{*(Dͼ9W+˔b9toғ=k/ @[dMh6dkk9eJUWW[ HMMU PyXJsB 1֜$|>CMH%&&hw&~OvSq)>W3oz19v]۷W]}3Sv4Q+,+PfjGC1d~pǜfSFF \r|C}q~Myrӯ! ! h}6`u MRZZ NvhV2+_׿x4BK]m۶4`P\RN:D=$ñF?vC2l6H^CRqqv=>I߇e>/\^iAzX33QY]~R'Du=B1QhLʠݮY2 Y[̐:gn}J4pfՇ?2<h\*O7ɑ)_YMheMx]ΐk᳊~:~ZܫW+kʻ=k0LkALESҟn3%Cy?셇AΌeMyWec.wzuJK qbⴭ`V~8gJҦMPſgo%gVx_{W9p(AJ<{}/'Wrx_ߔww%{JZqXԂ U[/uջk/lBCqUJO6<_~i"}N6X)b<}7UjIReU9N^oo#o.%77,WQ!xi9eOHQG6Td;By7˳cӁ|^)A Y8޺FCڕKg&_M=x. wvTVV&gxn6`8-#++K:a@C\.-+4`@RRNcu*0 ::Z)))*,,:t:ph@А! !BCBٻfbDQȖijyZN'ӯR~撩IeeffYvL%@q MDPQ@֜L f.ZѮyzM֭SNTUU 6h…Zl}|ĉZljǥ*6&FfYwl $IO7RYRM8Qw~zoo>|XÇ?;we6yZpaѐe`08f9//O=zІ t4~!fҐ!C·?VYY)%9YA3gR\\rssծݭT_IO>{VX+V|l_T~Q``?"M ꔇ+Po=̺UTVe9Ƴ'OEᄏ85iDs̱?۸q˲<==ر2]}2Y=ާ,Ə"mIg~a=3h'l6) Y%|! kWeRϩ>նl:ٱcǴtR >  ܹ5fƏOVProӦ'IZ5wuޣ'OJ:{"M{^}眘 ,@q1XI0z(<8iu;Hoٲejܸ5kvQiiijӦs 17~>sN#;]g|,=z?NINͭZO@k֭u YsڈkkWRVyGV7$$D'OvfiܸqZtE|Ù^oo6mum,/33Spooo?̟/$TivIl6NUW]b̐hTtƺ&2Ju Yf|=kW^Q2z2M+ {-[TO`8o?@7p̟'IY3UXX\FYYYuOCƧ հA`󪪬zo&iJ1C۷mnȵ=٬GЂ^Syy$ǽ{5|س2zfP=عVY3А@q1XI0*<Կچlm8~\zz Y/[ڦKB4{<8|*++5bKZjڙzZժ۶Uu@CtmtXJJ+uW|Λ'___Ko֌s>M ~if8r֭Rob RӲl2 jwk{ݽNhx7ZfiԩZnKԌ IDATkkXޒݻӴkNuM0;M֭S-odfd{uw @'$\B+WvoujMSw`p|hА! .BC\Ny X*+R=NА! .BC\ ?`bcbd6}v-\%WQLl*+-J3Fyyy@CpYfiȐ!qE딒,f)..N]4fL6}I,xZhl6 5i$Wg޽{լY3fM6M /fcǎٳFN8cꡇRv픔^˗/vޛo^zI$Kf7*%9Ydt,.<<<\aaa|je DGGkȐ!ʕ+5rHXVb k;v^x[&IrԽ{w 0@}UVVt颏>H6l/_~yVRΝ%Ij߾kg@R@@5o_=o:*P+c5թ>=Iu YfQ~^^5Ϋҙ -**Rqq$iӦM=z}Ȑ$9rDvRyy$)++KU&M4g7W'..y_|y͟?_m۶UjjʜtzeXHJ:}}}GVYY~[3j], UYz7㔺6c6_eXΛc0sN5ѼtnZ<>Lf?A\ɓ:i:bӌ$Q V_wwjOKKS6me}6?駟P\\nZfƍ>zgx $kw딁7꺫qޙr9uiug۪Paa&M'O*!!AΞ6mRRR|r-^Xk׮Ւ%K.8o˖-z7j:]yQEFFS$I):uIӧk7~l64߫2]5\*<<\EEE*))qw8 $$D6MGxyy)44TFEGG+33QP 111#::Z٪tw8 ""BO&44TEd2)??Qooo+IW0LБ#GաCǫ:[ddλcXti֘f^\W7oΝ}; +z **J4c/3\Y ɓըQ#JFGCp#G;\+Ehpe1ꍠ:QZVӧO;ڸr*,9'dEhА! .BCC-؜8,Zޏ=(///y lJ=4 lVNvfϜZ,P';߀ڶm >Ј빡CdYuku Y\4i3fhڟzHrO;v蓕+ԵkWFi)0{ f~wuuϋU\\,ѨFtMd23թ>'dpԴi/J>ѣʕ #<׳C*';[CnN WY3:mƍ۶)++NqB!o_Y,Ȑlk޼EG+7b7o~ܻWÇ=+Ѩg Ճ;kgz?7Ƽ8JӇבu;{FQ6A{vքy?Vo۪VZi,@q1XIPPX`CͦX̙3$$///IҞ{u5ؗ:G`P c0:,ꍜѸaK˕ﴺэkذaz7W\Bц lR$IIO8-ܯEҵ+*a0(mw֭[[= aaa矹%"; "ͦwG\wGlwGF(edd;lUVV; ; *ŢBwGe2`; `2#G; C;Ǐb; <;JP=˕uCkYڸr\,x\) W! ˊߠ! .BC\NyJTUU96{! .BC\, YpȣSh46ŋհa~iSG8Y嵮 Yf͚p믿^w}'Pl쵺C9SzKj;S-$___͛7OC5tPkNIII֭$cǎZp^uM8Q5eI>EEE7x~رCU>QTTTCh4*&&FE맟~S-,@a|}\Α0fyϾްA_o:\Y .BC\, Yp  Za4:6\ } YpjY4dEhА1;pXjugOj/s,Zޏ=(.S-dRyYf&%鮻փ}Q˜k],puM'LѬ,wGC6\"<<\Zߢ$oϞ]wiϴzgyѱcuECWZQQQAaڵkZ^VZh4Ú-[K׮HiFR}?M6MPTF y󔚚.]*66V͛OW6UEEE'OGCݭjݧ߯oAfIU^+6R5bHyyyWo4u4>|X tIhgUTFٳcf'OѨ[om#YGԬys-zsx{{w虁T,xgΜ<7/ڴmIwyG491QQQQڹsv)ooo͞3GK?=5oII:x𠮻:=3:xtjJbN<`O ҙb~krof}z.X II?oza= 0A-[Tjjyynvޒ$fL&Y3M6Mz%IQQQPz'\{p)Y,h^̞zsϙsMeXT, I&oY-UU9nii.xZjii*//`P=TZRX0@+WԱc_k{<(ITXhh7k4=}}9xaaÇK={Bjl߶M&L>]J2L:q232$I驫dR^n},3#C&Y~~~򝷎 S4xӲX,1r H:{2Ν5\,[G%;8y֪*%NPC ׵Y:O֯['I԰aC1B8e}M۶m <\3g̨vϲ!PRRrXzzmNÞ{Nׯ׺kqns3f4/ʶۼY%ot7dopSNչ)q1XI2zx+f󷿩Yf$I:qℤmѢ$OS\wWZZ|$I:tо{l߶MsfV۶m%=-zJnOv,&6Veee:s˲]*'+%IM*'Ï>+V8'dPo9F8LeǝZ{@6lrfI O?ӧiTp@ L4ik],AY3}Tj{zzJTUU YVũI&3g}ƍV^ii|qgV׮ݔ0])))6mڨG3fl٢-[nѢEZhѥ::tW;AOtZ-,$ױcܘNϟÇD&Iyyy:ucO, i޽߿c2Õ"4dEFP@@Zwh`pJm\NV"4dEhА1;\J xl6+';[gPEE'ԲU+ TkqB䌖~F}o޽[2Ξ45j,X~[_s)--UJr c9pիV, 2{x]tG}B 6L˗/;zwoN>UVsΒ$o^_$Kk֬${WAAA޽ }*++کoN:ﯞ={*--Mh}߿Əc:rf͛+99YM^SJxe:Uӧ')44Nse1ͺӯyVURQij[, KSsUvv ;?ڶmTItɓh"}wԤI͙3lƍ}WԴiST||o%KTTT]viĈ5'N%IljrY+cjFR0]3gиdٔ)>Ֆ!,,p\xxTRR(p@HHl6 .o~󓏏E!^^^ UvvFQtwBLL222Vvv*++PAA $9bQ@L&;*;(pdRDD9(pPll:pPTT?D<;e:",,V͛sj߾}ZW ,jajԨ&M(h0rHwGWАe`08f t\Y z#( )תvm$*,9'dEhА! 8 (('brw ljIzzCޏ?&d6+55EKzK=d6mVu Y7&N|*m 믿^͚7W@@𧻓_AQF馛T ͱ3st-h ڱc~^7NT[oٳ5t0~1hrb߯oAfIU^+9WZZ]$I6M'NꪫMqqJINVqq$u{;VZ$}i>ѣʕoY?'\?9kn>qF0b^3FO(Zl鎯nЮ;|X~^®@a4hq"[eSK[,g,,,_]\>|XtQݳGl85#l6#T^^j YQ3^&٬U2QVΝ/{^RRr56-fFzIk>_];S%Iyj$>'4,7.+I<|Zug3Lrި_f^9R~6o?O۵K7t*IW?QZ@Qq*[A8j)SeqSk/CI+4{mذY\^:wZ*"=֫$i46o]M:MeJ۵mml Dž fSAA~~~Qns Rhh0VffbbbpPtt} ꯈQPY,; (Tkpiy{{+88X999L&EDDȑ#СCEEE\'YANPfVCkb48u?+o Yp"4dp1b)2&w~u)Xmw-,*PqB4Vmh5! .BC\, YpEՊ+NC{p;oj;~\,pC6`@===;tzzLM4QVTTǜeᩧ߯+((H}fSaa&M\wyRRRj], )==]oBBB>}X_5rH=sh=s:x𠮽ZM\g}& U ^~ZP(r@@~ $B(*"BED 7%L200 \f&pfk?{ג^xA`P---JR tQG)#T<jQ[bqg`pVy2 9y:>V.:^Գ |4ykQRiŋkժU[1<<#^YyP'3@ѢoDOWKK$oy晽IG\.t:x<]vIzzzb :1/,I:/{Y3~=#.+kNFJVuڵkumP((H:sdB!]}Ւ}{ZfnvR)=Vq٬*=*Od0g/V%YO<}{~LLLhʕ*U(}>U+)Fa@jYdFFWW% U*4fyg3HsސX^ YdFXLZ3i1Y[Fi8Hmݲ6¼p/w$mtu `ri~* ֠6/o;> kYɔXp2Ly.?~~_9|xCsrrwa\fp:S?b8|qmEu8L-%P+j\i+.lyU>@Ud0Vj\N7&&&f3v9kLX[t;ߩ{7nJ^LY 9a +o{}цaf3ڲesU Y@C?*`xg+Z~ @0 5€,1Oկy @x0e"J>r8M2V Jސa@jY,8}5YdFs,X,7, gqYÀ,d2inݺuU #8B+[Yŀ,SN9E'x֮]>[ַU/|A^{BP9&n^V*7XpŢqJ$Zz6lؠ6s9ujz׻Ub.ЪUW S* D5˱׽uZ~z衚SNzZ `i?v_Qu ׬.藿>y,Yh7mo$G_W/V6ʓY3$q[*E"r9=Ӻ+߯]__>o f'3.M_ɍU 6;<~ꪫt 7쩛h͚5o~Srp1 9#M -;wܱ}Q=Ӷ;3Y-49spP~0 5€,P# @x0-J>dRR(MsS"pUǡg"! 5€,Xx[&\yo>q:z]\w׮؋z>h60nqgM:#;Za&Iz+Vh{%I7ֶ69Ξ щo>I}Y3↕-IקoA*I{֣SOލu7VWVPPq3O?x`Vy2 祷M|xjEQIs?Ϳ|~uȏG~ɔP# @0 5€,Ԉ S&S*Ӥ2ŪL YdFa@jY57 8q}t-ŴqFxk> _UWd@8L&?[A)}_>]tJU*OdIs>+_7Jm׳:K'x֮]>[t;E]y ;#LjӦ亮.]~YUnC{_z]{]}ZrV^K_$Iz?p ׵ި|ѿ^|Yǀ, 7( trbTղwܩP(^xA`P---*q]p:ꨣdtG*3 ;=H\gqw7?jl:vrE:߿{B`]qTT^sN;M/֪U$Iׯ.sORONQy{~ ޣ\>'wYtuuU *L*4*0!5>)07577+2o<٩FW\lҥKyFW488R栶4>>\.@ggŢDm۶Ue˖iӦM khhHbU%KhddD|UYx\PF5¡)Fa@jYdFFWW%l6B`)USSSUǡg"! 5€,P# @0 DZHWA{36_Z kZinn?!ݴ3yݭ:u^|mrn{)9O]wtzg Y@˗'IR8_/^i=f3U^ `Θ`$9`S@F⭭r]WJ)hѢEKnWTg[uYWvcW哳Ώ5d0ggkXQ!Qz]~rs}m[ZZ{nIRww/_o]]]z_/IZlrt W~#Uɓ7d0g[fk-tIڼiz)=Zwc׿$K_m7lؠ|A]}53Q:C??|;h"B!}ob:ԱΗ$=Ӻ6ud)g⧏>g~ZT.Eܹ IDATji.Їeqh{ݨ{X<jYdOX,U, P#^+LH$OX4NfUTR>07dFa@jYdKXL7no_HD\r~׿V%?dXn݂.w%ꑟ<"ǩN~^u:uWxN'?ѧ?i|3uꩧcc=fQC! 9ɍɏtt;\SHT /P5k'І tw}ݧ5koW*҃>yiڵT(H$tuIַoo<Ӻu$i^k'.RTu`v.^ڍU\eފ˵Xp׶<_Ii]]]JRVsxW*>ܗ};}m{kmmL$ ,hO/M˪ԤH$a}bs<ۜ7le/׭mnPHڹkW}sgOޖ@ >m[]maw}bq߯m3Y%R΁,^XvR6ٲ,c[yyd^4ljEL#nooWW&1Lـ{yqϵEi||efz:}DҢCu'4)˰_юVy?k]:=O}9Bƙ[hFFU*jikNi.inn>mݦ֯~FWP7d[#mn /j3Ne^inh8/| RE7N7 ihBm-'c:|j/k`d2VzfE!&8c:|Ԣ Y$<*2;&2h4آwi8gXR0hޥ)[ue۰6$ma:|y72RWٴ~EM54To̦$u sG*Ws,l&Ϋ,$m9nc%s`ipGHD[VEYv_ڴ]*SPs7bH$T*cHxJ]S]PcH$w +J4PϘ&녭տ8=GL3×JQ{E{׀ElR&(Z1t6@$cj7?.m4bY~5j_x9ob_J&dYVF477+jxxUp8N 6*0ຮe˖FW.]͛770400A,~Ռ9hNE%՝ *4::@$3cΝ x>m۶Ue˖iӦM khhGGh%KhddDys0s_;0]wuz] kW\*5XL8 dFsF[<^|HDU*Db,P# @0 5€,`U<,p?/Ibgg7&uDua?^~ǀ,` {A;NuV UO?crU'`~;ԛft*SOՙgD"~vhTs;Ow\֭[uڵRtj}˧w:,t:tO}J?|yu-7+ QuFdyCtϷwCzϹj겏|DLo~S/TZK?SOIڪ3qcސB~ەƻ;F=#7Q|Ie2Iң?U{qtλ߭#sng~P=~}_7t>} UW~otƍuzwW1.z{{L&=Ck||UfEQ 7*0٩FW\lҥKyFW488ߛB]}}}W.ktU`SbQDUVyFW"۵sFWIm/:[#4J٩֪CyCjYdFa@jkt)DU i8U)RX( O:8]2SK$b.)n.ϫ)6NW}'iSS&iFHdGy-[XT$4JS*ey5[Ѻ 5B3>}Y ;lAYSZ,cSR"5>S((fOL; <:岂dzy#ٟMwSL@ X,V9P(2l|gޣ۞lommm\nҦli~4mmm7>ǭsbilڻUʾOL i{bB1v=ELc+FJ T*F$FL3i{al6[LgzxH$42f:k8jii)zGD 5JY_%ߗgl<†Kiy}_#m{(W* _le{J%,9 i$*+W*Y_lM#܏iQ)DWX]i")MU5f:q4*a71M{[C6T.+[(bwe bWWLF庮t˚BL3 +5N[=oG%ydf 1;lG)p*U4{;l'&MZذiJssc^%Ie2FW:::]hnnV4ppXltU`u] h˖- ,,]T7ont5`h``@*Yקq, q:;;U,jVyFW"۵sFW!YR{{{9YB,-j".R}J~ 2?nݺKOT'?:[,b[sMoz֬Y+WMCCCG?h4Kgqb>׿?pjڵz%I^xN8͛7k}4guNqF\R>j]ve:y?i=Xs=O~Rowu."\R:_3w߭D"իWkÆ ϩ Y!{Zt뼧~_erU+wᅦyfIҶmSO)Ko߮i釆K6mڤnI{.uQd2:#5<<|4v<01 9c&pU})birNŋj*I$mLY 9fJexf]vx<7XbF"Ȭ@,挳~|~衇j=YF~R|۰aN=@9]]]~+qU2T&itU`Ck||UfEQ 7*0٩FW\lҥKc``@*J i|||SSanTXT"htU`UittUH$vܹUקm۶5*0l2mڴՀ~ hU-K,ȴ)qQ]Lx &)ըF8T1e1H],#U*q\V 0NW*r,}h>vR)_/~7}b$W:V(P6=|W"0'\:ڦ)r-F1fu|YϾ/qT"\%75 )'fy~۝VIJ˕u:O%R4Ắz{{i Xv_ڴy3cqh"Eڃu]r4j''4Yz1u\o|r cV yeo6߳LKg477+hr˼OL*:CZZZԶ9}>!34JE~5phlPoo6NH*bnbtRm钓)63Ѩ&Ǔ E̛artRmb,ql6h4lX,* H)fm BL&+3f9Ś"Fi$ir,VvMgSRl?ߞ<5bu|L^јy#ٷHBm]Rɴ[ȧi5g^V P8ථS BaD,{1m㨩IVeҦli~4---zt2Qɾ+jLԤ]dw 'F5Vv=CLc{c/yfP)P \^|lBe.aO&h1qp(ȘbǤ4>r$Fr>1"IIE;e}RN$Uw>V.c8Y9Qa屜Q"^X1M.RndLy?R8)27[edAw(oq=bq4%˘Ψ`*%)3dRjxh}T^1e{;)'S*̼ XlX֔z4\n1M8#/)7:fH_YJ&mc65R( F8*U4/'$y%&+{9Mqt\NVט&Q⍷ظ*Ibl(b6gHv14PP ͵]ZjMLJ:djŮd6Igơmyouvv*N)3٥˱xR%/BoZ^<'(ML&_Rm:zZ2>ЫIDATYbD"+O_漑I=o6_CZ[[5{hN S*~Yc~e[ uh"رc^|7q-^Xw9`*xin+fo)P44ݚTXKyis֦bt:mrߣJ 7iiiQ D˚BLtf@8V<Vٜ}1^@@98X4{Ӽ0~9\noy,ۻa ^l񣣹~:e4===t6}%wlQCK]ߐU-u-r.J㽴l5dJsѨR!;arJ⺮ 4j3i$ʋHLtx<֡!,NL3MLԤȰ\밤B*pcĦ2h4|*:m91t1M8f5/k 1ty jmfjFbLcqܬf 1;lGkk;v9͔z4|NM{'1MKKI٬Khf:@ssFGXn&JzKQCK]dJYqrLT+2*ŒܠŮ1\ڪl*B|R o|l8\0 \((cqCXY\L/MMM}_Txr>)X6]&;( )12ž9˥tm ye-RIa{[Ǫ\,)`пzJ<䮝Vf'r)gŢ\0&Ǒ٩ɝ;ʲnٮ5b[CVl.۾r4ng1~ʲi6%[WX1m\oY%WlnV1_Pb_f:Ә&*(Jռ)4әPHXL8H"y%u崷+18X4Sie{z4}G]LgLc}~ ^8Pٯef 1t6}CQUlYi| WJ,rNU5d睎֐g yYhTì!;auvvj5dum۲UwRmݼՀ!`@!;4Ng'kG/!;J$Q{{vyJ}}ڱ5deھiSC~ TZD#! 2: R|3vob=e'dSa=:rZ , jKG,v[>in}UbM}GcO,ۻIΛ ;*2o=̏z-O#m. -/moWI˲2ຮb!ŋMu>/uX8t{,ڮﺦ1M75R'1MuʊuT.rZ<b"cs[D''n|ؗ:,~9g(:ILD"5Z4)LF<źB|GLD2nO|TfzC{M*,dsؽhLV>SXaFLtuu+ca3NWHjXgupyb`C鮼B$b|pbX++ (T̯zTIf:& ObZ^ ȗr,'`8N@|E߭()d6,gԫ]ו8N(/V 8o$)(*lɒ­d+,KLYWu庮#Yj~O}A;WQ bq~[^ eZ-TjގůTe z*r\J>ݗb^qRTYs"-/lB6P Yy % l4?ڻ:6k)=u#L _JIke yyir),ڻO*n1NW.FXT ͼ,qD%1⃚JM*xXOʥ\4j/Iۿ[1j>P\Ԓ@X²"]vg48;ιlgޯgN8;9sfD}fs9?'װNS CIܐ YWcSWJN|qU/.,@\aUnO"utt?0~AXr* Q|06?E[. Bܐ(oN$jmmmV;)oI*{rayr#[ԱySYӾ ܈c[Q>gʏRc~^cQVD,:NS#1X(<i$NŢNM׍떊[o0QwwF'RyfRN3c >ͤ4;Mū7< z=V֙3gey$yi&ijY]]:팚,,&6ot/rr9f ї|WA%K/Y~{ Joz9WA9q O펼a~[O,s=Gay#`Nz͍z<{*IsTwT)Y!oQhš_T1-wTKCvsvMsNSNs;DiX0\]I2^WiXX8aijij5wX$y9OAM: ԮNs2Lh֒7do^د|+T8 8j)^Ia}U7FT[JS-)ED5?!ǰO|YnwrmTqB,}UPNߖ U }YurrBK}b{\߳eH2 ?$:j޸\E*GzT5sOzj-]TCGhdd+_G|+cu׊422G=|5c ۷/$rzr뭚{\ݻ71ssG>KqR-[L|AH{XMܦZV%&IbM$''ɋ;7I:}չ~s#6^$$/&@ހfuڵk:;:/vmKtd6mܨ6m}V5\kV<:::uv꾎xŝs422nm۶]kܛКZܹMwĉ/RK+;7a&:|pCj qv\=rGӟbhM͚[e%;;Izn$YqF?IGܹOaͭ޹~s#&Kwvttĝh<8>t'5::*IU__~˒ ޓ$ɓ3gNs;?6v\NgϝKq4#$i_دz;ős޽{HXܹ j]wu[駟;aͭ޲ĝ$=qg'Owntĝ$';7I:M%&;7 hu/e2VHn###z`:m޼I߹ᆬВɭWgKC;JKi8wēOX,j`Vh9[=l5$I6qIFɍ~Frd$Jn_>OHn_:Hnl5$;F77dS266M*$ӗ<ʠKgΜ:Hn҅cz7t75U;owk$ }!x᷒_oؠ>:t(AFr[ꪫ̙3qf=Ѓ:'w9C/V#It,5$;F?N#OsMu.dܾu$%j$;Ivɍn4Y%KO>QRZ`$iE$I5{;Vu+jnjppvڥwy'MoiQs;w/_^Y+Kf95i֬Y/411ɶ2|;x$ĉ:u:;k3ɪ[=#jvt#jv!jn2'٫[sdzdzddz9 l~Yr99sZ۷o$͛7O>~~}:t>xl٪B߽CjH5VE4WVHi e9am֭/Vke5cȑ#ڽCmٲU Ys{jppn* ڳg3MꡗdϚWd̚$['ٲF?V\F?ɖ57L7ɐ57zI M2d͍n4g֬Y4TH,pC YH7d !r2IENDB`bird_exporter-1.2.2/main.go000066400000000000000000000070501333773000500156420ustar00rootroot00000000000000package main import ( "flag" "fmt" "net/http" "os" "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/log" ) const version string = "1.2.2" var ( showVersion = flag.Bool("version", false, "Print version information.") listenAddress = flag.String("web.listen-address", ":9324", "Address on which to expose metrics and web interface.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") birdSocket = flag.String("bird.socket", "/var/run/bird.ctl", "Socket to communicate with bird routing daemon") birdV2 = flag.Bool("bird.v2", false, "Bird major version >= 2.0 (multi channel protocols)") newFormat = flag.Bool("format.new", false, "New metric format (more convenient / generic)") enableBgp = flag.Bool("proto.bgp", true, "Enables metrics for protocol BGP") enableOspf = flag.Bool("proto.ospf", true, "Enables metrics for protocol OSPF") enableKernel = flag.Bool("proto.kernel", true, "Enables metrics for protocol Kernel") enableStatic = flag.Bool("proto.static", true, "Enables metrics for protocol Static") enableDirect = flag.Bool("proto.direct", true, "Enables metrics for protocol Direct") // pre bird 2.0 bird6Socket = flag.String("bird.socket6", "/var/run/bird6.ctl", "Socket to communicate with bird6 routing daemon (not compatible with -bird.v2)") birdEnabled = flag.Bool("bird.ipv4", true, "Get protocols from bird (not compatible with -bird.v2)") bird6Enabled = flag.Bool("bird.ipv6", true, "Get protocols from bird6 (not compatible with -bird.v2)") ) func init() { flag.Usage = func() { fmt.Println("Usage: bird_exporter [ ... ]\n\nParameters:") fmt.Println() flag.PrintDefaults() } } func main() { flag.Parse() if *showVersion { printVersion() os.Exit(0) } startServer() } func printVersion() { fmt.Println("bird_exporter") fmt.Printf("Version: %s\n", version) fmt.Println("Author(s): Daniel Czerwonk") fmt.Println("Metric exporter for bird routing daemon") } func startServer() { log.Infof("Starting bird exporter (Version: %s)\n", version) if !*newFormat { log.Info("INFO: You are using the old metric format. Please consider using the new (more convenient one) by setting -format.new=true.") } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(` Bird Routing Daemon Exporter (Version ` + version + `)

Bird Routing Daemon Exporter

Metrics

More information:

github.com/czerwonk/bird_exporter

`)) }) http.HandleFunc(*metricsPath, handleMetricsRequest) log.Infof("Listening for %s on %s\n", *metricsPath, *listenAddress) log.Fatal(http.ListenAndServe(*listenAddress, nil)) } func handleMetricsRequest(w http.ResponseWriter, r *http.Request) { reg := prometheus.NewRegistry() p := enabledProtocols() c := NewMetricCollector(*newFormat, p) reg.MustRegister(c) promhttp.HandlerFor(reg, promhttp.HandlerOpts{ ErrorLog: log.NewErrorLogger(), ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(w, r) } func enabledProtocols() int { res := 0 if *enableBgp { res |= protocol.BGP } if *enableOspf { res |= protocol.OSPF } if *enableKernel { res |= protocol.Kernel } if *enableStatic { res |= protocol.Static } if *enableDirect { res |= protocol.Direct } return res } bird_exporter-1.2.2/metric_collector.go000066400000000000000000000046551333773000500202570ustar00rootroot00000000000000package main import ( "github.com/czerwonk/bird_exporter/client" "github.com/czerwonk/bird_exporter/metrics" "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" ) type MetricCollector struct { exporters map[int][]metrics.MetricExporter client *client.BirdClient enabledProtocols int } func NewMetricCollector(newFormat bool, enabledProtocols int) *MetricCollector { c := getClient() var e map[int][]metrics.MetricExporter if newFormat { e = exportersForDefault(c) } else { e = exportersForLegacy(c) } return &MetricCollector{exporters: e, client: c, enabledProtocols: enabledProtocols} } func getClient() *client.BirdClient { o := &client.BirdClientOptions{ BirdSocket: *birdSocket, Bird6Socket: *bird6Socket, Bird6Enabled: *bird6Enabled, BirdEnabled: *birdEnabled, BirdV2: *birdV2, } return &client.BirdClient{Options: o} } func exportersForLegacy(c *client.BirdClient) map[int][]metrics.MetricExporter { l := &metrics.LegacyLabelStrategy{} return map[int][]metrics.MetricExporter{ protocol.BGP: {metrics.NewLegacyMetricExporter("bgp4_session", "bgp6_session", l)}, protocol.Direct: {metrics.NewLegacyMetricExporter("direct4", "direct6", l)}, protocol.Kernel: {metrics.NewLegacyMetricExporter("kernel4", "kernel6", l)}, protocol.OSPF: {metrics.NewLegacyMetricExporter("ospf", "ospfv3", l), metrics.NewOspfExporter("", c)}, protocol.Static: {metrics.NewLegacyMetricExporter("static4", "static6", l)}, } } func exportersForDefault(c *client.BirdClient) map[int][]metrics.MetricExporter { l := &metrics.DefaultLabelStrategy{} e := metrics.NewGenericProtocolMetricExporter("bird_protocol", true, l) return map[int][]metrics.MetricExporter{ protocol.BGP: {e}, protocol.Direct: {e}, protocol.Kernel: {e}, protocol.OSPF: {e, metrics.NewOspfExporter("bird_", c)}, protocol.Static: {e}, } } func (m *MetricCollector) Describe(ch chan<- *prometheus.Desc) { for _, v := range m.exporters { for _, e := range v { e.Describe(ch) } } } func (m *MetricCollector) Collect(ch chan<- prometheus.Metric) { protocols, err := m.client.GetProtocols() if err != nil { log.Errorln(err) return } for _, p := range protocols { if p.Proto == protocol.PROTO_UNKNOWN || (m.enabledProtocols&p.Proto != p.Proto) { continue } for _, e := range m.exporters[p.Proto] { e.Export(p, ch) } } } bird_exporter-1.2.2/metrics/000077500000000000000000000000001333773000500160335ustar00rootroot00000000000000bird_exporter-1.2.2/metrics/default_label_strategy.go000066400000000000000000000012431333773000500230670ustar00rootroot00000000000000package metrics import ( "github.com/czerwonk/bird_exporter/protocol" ) type DefaultLabelStrategy struct { } func (*DefaultLabelStrategy) LabelNames() []string { return []string{"name", "proto", "ip_version"} } func (*DefaultLabelStrategy) LabelValues(p *protocol.Protocol) []string { return []string{p.Name, protoString(p), p.IpVersion} } func protoString(p *protocol.Protocol) string { switch p.Proto { case protocol.BGP: return "BGP" case protocol.OSPF: if p.IpVersion == "4" { return "OSPF" } return "OSPFv3" case protocol.Static: return "Static" case protocol.Kernel: return "Kernel" case protocol.Direct: return "Direct" } return "" } bird_exporter-1.2.2/metrics/generic_exporter.go000066400000000000000000000246011333773000500217310ustar00rootroot00000000000000package metrics import ( "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" ) type GenericProtocolMetricExporter struct { labelStrategy LabelStrategy upDesc *prometheus.Desc importCountDesc *prometheus.Desc exportCountDesc *prometheus.Desc filterCountDesc *prometheus.Desc preferredCountDesc *prometheus.Desc uptimeDesc *prometheus.Desc updatesImportReceiveCountDesc *prometheus.Desc updatesImportRejectCountDesc *prometheus.Desc updatesImportFilterCountDesc *prometheus.Desc updatesImportIgnoreCountDesc *prometheus.Desc updatesImportAcceptCountDesc *prometheus.Desc withdrawsImportReceiveCountDesc *prometheus.Desc withdrawsImportRejectCountDesc *prometheus.Desc withdrawsImportFilterCountDesc *prometheus.Desc withdrawsImportIgnoreCountDesc *prometheus.Desc withdrawsImportAcceptCountDesc *prometheus.Desc updatesExportReceiveCountDesc *prometheus.Desc updatesExportRejectCountDesc *prometheus.Desc updatesExportFilterCountDesc *prometheus.Desc updatesExportIgnoreCountDesc *prometheus.Desc updatesExportAcceptCountDesc *prometheus.Desc withdrawsExportReceiveCountDesc *prometheus.Desc withdrawsExportRejectCountDesc *prometheus.Desc withdrawsExportFilterCountDesc *prometheus.Desc withdrawsExportIgnoreCountDesc *prometheus.Desc withdrawsExportAcceptCountDesc *prometheus.Desc } func NewGenericProtocolMetricExporter(prefix string, newNaming bool, labelStrategy LabelStrategy) *GenericProtocolMetricExporter { m := &GenericProtocolMetricExporter{labelStrategy: labelStrategy} m.initDesc(prefix, newNaming) return m } func (m *GenericProtocolMetricExporter) initDesc(prefix string, newNaming bool) { labels := m.labelStrategy.LabelNames() m.upDesc = prometheus.NewDesc(prefix+"_up", "Protocol is up", labels, nil) if newNaming { m.importCountDesc = prometheus.NewDesc(prefix+"_prefix_import_count", "Number of imported routes", labels, nil) m.exportCountDesc = prometheus.NewDesc(prefix+"_prefix_export_count", "Number of exported routes", labels, nil) m.filterCountDesc = prometheus.NewDesc(prefix+"_prefix_filter_count", "Number of filtered routes", labels, nil) m.preferredCountDesc = prometheus.NewDesc(prefix+"_prefix_preferred_count", "Number of preferred routes", labels, nil) } else { m.importCountDesc = prometheus.NewDesc(prefix+"_prefix_count_import", "Number of imported routes", labels, nil) m.exportCountDesc = prometheus.NewDesc(prefix+"_prefix_count_export", "Number of exported routes", labels, nil) m.filterCountDesc = prometheus.NewDesc(prefix+"_prefix_count_filter", "Number of filtered routes", labels, nil) m.preferredCountDesc = prometheus.NewDesc(prefix+"_prefix_count_preferred", "Number of preferred routes", labels, nil) } m.uptimeDesc = prometheus.NewDesc(prefix+"_uptime", "Uptime of the protocol in seconds", labels, nil) m.updatesImportIgnoreCountDesc = prometheus.NewDesc(prefix+"_changes_update_import_ignore_count", "Number of incoming updates being ignored", labels, nil) m.updatesImportAcceptCountDesc = prometheus.NewDesc(prefix+"_changes_update_import_accept_count", "Number of incoming updates being accepted", labels, nil) m.updatesImportFilterCountDesc = prometheus.NewDesc(prefix+"_changes_update_import_filter_count", "Number of incoming updates being filtered", labels, nil) m.updatesImportRejectCountDesc = prometheus.NewDesc(prefix+"_changes_update_import_reject_count", "Number of incoming updates being rejected", labels, nil) m.updatesImportReceiveCountDesc = prometheus.NewDesc(prefix+"_changes_update_import_receive_count", "Number of received updates", labels, nil) m.withdrawsImportIgnoreCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_import_ignore_count", "Number of incoming withdraws being ignored", labels, nil) m.withdrawsImportAcceptCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_import_accept_count", "Number of incoming withdraws being accepted", labels, nil) m.withdrawsImportFilterCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_import_filter_count", "Number of incoming withdraws being filtered", labels, nil) m.withdrawsImportRejectCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_import_reject_count", "Number of incoming withdraws being rejected", labels, nil) m.withdrawsImportReceiveCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_import_receive_count", "Number of received withdraws", labels, nil) m.updatesExportIgnoreCountDesc = prometheus.NewDesc(prefix+"_changes_update_export_ignore_count", "Number of outgoing updates being ignored", labels, nil) m.updatesExportAcceptCountDesc = prometheus.NewDesc(prefix+"_changes_update_export_accept_count", "Number of outgoing updates being accepted", labels, nil) m.updatesExportFilterCountDesc = prometheus.NewDesc(prefix+"_changes_update_export_filter_count", "Number of outgoing updates being filtered", labels, nil) m.updatesExportRejectCountDesc = prometheus.NewDesc(prefix+"_changes_update_export_reject_count", "Number of outgoing updates being rejected", labels, nil) m.updatesExportReceiveCountDesc = prometheus.NewDesc(prefix+"_changes_update_export_receive_count", "Number of sent updates", labels, nil) m.withdrawsExportIgnoreCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_export_ignore_count", "Number of outgoing withdraws being ignored", labels, nil) m.withdrawsExportAcceptCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_export_accept_count", "Number of outgoing withdraws being accepted", labels, nil) m.withdrawsExportFilterCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_export_filter_count", "Number of outgoing withdraws being filtered", labels, nil) m.withdrawsExportRejectCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_export_reject_count", "Number of outgoing withdraws being rejected", labels, nil) m.withdrawsExportReceiveCountDesc = prometheus.NewDesc(prefix+"_changes_withdraw_export_receive_count", "Number of outgoing withdraws", labels, nil) } func (m *GenericProtocolMetricExporter) Describe(ch chan<- *prometheus.Desc) { ch <- m.upDesc ch <- m.importCountDesc ch <- m.exportCountDesc ch <- m.filterCountDesc ch <- m.preferredCountDesc ch <- m.uptimeDesc ch <- m.updatesImportReceiveCountDesc ch <- m.updatesImportRejectCountDesc ch <- m.updatesImportFilterCountDesc ch <- m.updatesImportIgnoreCountDesc ch <- m.updatesImportAcceptCountDesc ch <- m.updatesExportReceiveCountDesc ch <- m.updatesExportRejectCountDesc ch <- m.updatesExportFilterCountDesc ch <- m.updatesExportIgnoreCountDesc ch <- m.updatesExportAcceptCountDesc ch <- m.withdrawsImportIgnoreCountDesc ch <- m.withdrawsImportAcceptCountDesc ch <- m.withdrawsImportFilterCountDesc ch <- m.withdrawsImportRejectCountDesc ch <- m.withdrawsImportReceiveCountDesc ch <- m.withdrawsExportIgnoreCountDesc ch <- m.withdrawsExportAcceptCountDesc ch <- m.withdrawsExportFilterCountDesc ch <- m.withdrawsExportRejectCountDesc ch <- m.withdrawsExportReceiveCountDesc } func (m *GenericProtocolMetricExporter) Export(p *protocol.Protocol, ch chan<- prometheus.Metric) { l := m.labelStrategy.LabelValues(p) ch <- prometheus.MustNewConstMetric(m.upDesc, prometheus.GaugeValue, float64(p.Up), l...) ch <- prometheus.MustNewConstMetric(m.importCountDesc, prometheus.GaugeValue, float64(p.Imported), l...) ch <- prometheus.MustNewConstMetric(m.exportCountDesc, prometheus.GaugeValue, float64(p.Exported), l...) ch <- prometheus.MustNewConstMetric(m.filterCountDesc, prometheus.GaugeValue, float64(p.Filtered), l...) ch <- prometheus.MustNewConstMetric(m.preferredCountDesc, prometheus.GaugeValue, float64(p.Preferred), l...) ch <- prometheus.MustNewConstMetric(m.uptimeDesc, prometheus.GaugeValue, float64(p.Uptime), l...) ch <- prometheus.MustNewConstMetric(m.updatesImportReceiveCountDesc, prometheus.GaugeValue, float64(p.ImportUpdates.Received), l...) ch <- prometheus.MustNewConstMetric(m.updatesImportRejectCountDesc, prometheus.GaugeValue, float64(p.ImportUpdates.Rejected), l...) ch <- prometheus.MustNewConstMetric(m.updatesImportFilterCountDesc, prometheus.GaugeValue, float64(p.ImportUpdates.Filtered), l...) ch <- prometheus.MustNewConstMetric(m.updatesImportAcceptCountDesc, prometheus.GaugeValue, float64(p.ImportUpdates.Accepted), l...) ch <- prometheus.MustNewConstMetric(m.updatesImportIgnoreCountDesc, prometheus.GaugeValue, float64(p.ImportUpdates.Ignored), l...) ch <- prometheus.MustNewConstMetric(m.updatesExportReceiveCountDesc, prometheus.GaugeValue, float64(p.ExportUpdates.Received), l...) ch <- prometheus.MustNewConstMetric(m.updatesExportRejectCountDesc, prometheus.GaugeValue, float64(p.ExportUpdates.Rejected), l...) ch <- prometheus.MustNewConstMetric(m.updatesExportFilterCountDesc, prometheus.GaugeValue, float64(p.ExportUpdates.Filtered), l...) ch <- prometheus.MustNewConstMetric(m.updatesExportAcceptCountDesc, prometheus.GaugeValue, float64(p.ExportUpdates.Accepted), l...) ch <- prometheus.MustNewConstMetric(m.updatesExportIgnoreCountDesc, prometheus.GaugeValue, float64(p.ExportUpdates.Ignored), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsImportReceiveCountDesc, prometheus.GaugeValue, float64(p.ImportWithdraws.Received), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsImportRejectCountDesc, prometheus.GaugeValue, float64(p.ImportWithdraws.Rejected), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsImportFilterCountDesc, prometheus.GaugeValue, float64(p.ImportWithdraws.Filtered), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsImportAcceptCountDesc, prometheus.GaugeValue, float64(p.ImportWithdraws.Accepted), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsImportIgnoreCountDesc, prometheus.GaugeValue, float64(p.ImportWithdraws.Ignored), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsExportReceiveCountDesc, prometheus.GaugeValue, float64(p.ExportWithdraws.Received), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsExportRejectCountDesc, prometheus.GaugeValue, float64(p.ExportWithdraws.Rejected), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsExportFilterCountDesc, prometheus.GaugeValue, float64(p.ExportWithdraws.Filtered), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsExportAcceptCountDesc, prometheus.GaugeValue, float64(p.ExportWithdraws.Accepted), l...) ch <- prometheus.MustNewConstMetric(m.withdrawsExportIgnoreCountDesc, prometheus.GaugeValue, float64(p.ExportWithdraws.Ignored), l...) } bird_exporter-1.2.2/metrics/label_strategy.go000066400000000000000000000005561333773000500213710ustar00rootroot00000000000000package metrics import "github.com/czerwonk/bird_exporter/protocol" // LabelStrategy abstracts the label generation for protocol metrics type LabelStrategy interface { // LabelNames is the list of label names LabelNames() []string // Label values is the list of values for the labels specified in `LabelNames()` LabelValues(p *protocol.Protocol) []string } bird_exporter-1.2.2/metrics/legacy_exporter.go000066400000000000000000000015701333773000500215610ustar00rootroot00000000000000package metrics import ( "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" ) type LegacyMetricExporter struct { ipv4Exporter *GenericProtocolMetricExporter ipv6Exporter *GenericProtocolMetricExporter } func NewLegacyMetricExporter(prefixIpv4, prefixIpv6 string, labelStrategy LabelStrategy) MetricExporter { return &LegacyMetricExporter{ ipv4Exporter: NewGenericProtocolMetricExporter(prefixIpv4, false, labelStrategy), ipv6Exporter: NewGenericProtocolMetricExporter(prefixIpv6, false, labelStrategy), } } func (e *LegacyMetricExporter) Describe(ch chan<- *prometheus.Desc) { e.ipv4Exporter.Describe(ch) e.ipv6Exporter.Describe(ch) } func (e *LegacyMetricExporter) Export(p *protocol.Protocol, ch chan<- prometheus.Metric) { if p.IpVersion == "4" { e.ipv4Exporter.Export(p, ch) } else { e.ipv6Exporter.Export(p, ch) } } bird_exporter-1.2.2/metrics/legacy_label_strategy.go000066400000000000000000000004371333773000500227130ustar00rootroot00000000000000package metrics import "github.com/czerwonk/bird_exporter/protocol" type LegacyLabelStrategy struct { } func (*LegacyLabelStrategy) LabelNames() []string { return []string{"name"} } func (*LegacyLabelStrategy) LabelValues(p *protocol.Protocol) []string { return []string{p.Name} } bird_exporter-1.2.2/metrics/metrics_exporter.go000066400000000000000000000004001333773000500217520ustar00rootroot00000000000000package metrics import ( "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" ) type MetricExporter interface { Describe(ch chan<- *prometheus.Desc) Export(p *protocol.Protocol, ch chan<- prometheus.Metric) } bird_exporter-1.2.2/metrics/ospf_exporter.go000066400000000000000000000047201333773000500212640ustar00rootroot00000000000000package metrics import ( "github.com/czerwonk/bird_exporter/client" "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" ) type ospfDesc struct { runningDesc *prometheus.Desc interfaceCountDesc *prometheus.Desc neighborCountDesc *prometheus.Desc neighborAdjacentCountDesc *prometheus.Desc } type ospfMetricExporter struct { descriptions map[string]*ospfDesc client client.Client } func NewOspfExporter(prefix string, client client.Client) MetricExporter { d := make(map[string]*ospfDesc) d["4"] = getDesc(prefix + "ospf") d["6"] = getDesc(prefix + "ospfv3") return &ospfMetricExporter{descriptions: d, client: client} } func getDesc(prefix string) *ospfDesc { labels := []string{"name"} d := &ospfDesc{} d.runningDesc = prometheus.NewDesc(prefix+"_running", "State of OSPF: 0 = Alone, 1 = Running (Neighbor-Adjacencies established)", labels, nil) labels = append(labels, "area") d.interfaceCountDesc = prometheus.NewDesc(prefix+"_interface_count", "Number of interfaces in the area", labels, nil) d.neighborCountDesc = prometheus.NewDesc(prefix+"_neighbor_count", "Number of neighbors in the area", labels, nil) d.neighborAdjacentCountDesc = prometheus.NewDesc(prefix+"_neighbor_adjacent_count", "Number of adjacent neighbors in the area", labels, nil) return d } func (m *ospfMetricExporter) Describe(ch chan<- *prometheus.Desc) { m.describe("4", ch) m.describe("6", ch) } func (m *ospfMetricExporter) describe(ipVersion string, ch chan<- *prometheus.Desc) { d := m.descriptions[ipVersion] ch <- d.runningDesc ch <- d.interfaceCountDesc ch <- d.neighborCountDesc ch <- d.neighborAdjacentCountDesc } func (m *ospfMetricExporter) Export(p *protocol.Protocol, ch chan<- prometheus.Metric) { d := m.descriptions[p.IpVersion] ch <- prometheus.MustNewConstMetric(d.runningDesc, prometheus.GaugeValue, p.Attributes["running"], p.Name) areas, err := m.client.GetOspfAreas(p) if err != nil { log.Errorln(err) return } for _, area := range areas { l := []string{p.Name, area.Name} ch <- prometheus.MustNewConstMetric(d.interfaceCountDesc, prometheus.GaugeValue, float64(area.InterfaceCount), l...) ch <- prometheus.MustNewConstMetric(d.neighborCountDesc, prometheus.GaugeValue, float64(area.NeighborCount), l...) ch <- prometheus.MustNewConstMetric(d.neighborAdjacentCountDesc, prometheus.GaugeValue, float64(area.NeighborAdjacentCount), l...) } } bird_exporter-1.2.2/parser/000077500000000000000000000000001333773000500156615ustar00rootroot00000000000000bird_exporter-1.2.2/parser/ospf.go000066400000000000000000000026241333773000500171630ustar00rootroot00000000000000package parser import ( "regexp" "bufio" "bytes" "strings" "github.com/czerwonk/bird_exporter/protocol" ) type ospfRegex struct { area *regexp.Regexp counters *regexp.Regexp } type ospfContext struct { line string areas []*protocol.OspfArea current *protocol.OspfArea } func init() { ospf = &ospfRegex{ area: regexp.MustCompile("Area: [^\\s]+ \\(([^\\s]+)\\)"), counters: regexp.MustCompile("Number of ([^:]+):\\s*(\\d+)"), } } var ospf *ospfRegex func ParseOspf(data []byte) []*protocol.OspfArea { reader := bytes.NewReader(data) scanner := bufio.NewScanner(reader) c := &ospfContext{ areas: make([]*protocol.OspfArea, 0), } for scanner.Scan() { c.line = strings.Trim(scanner.Text(), " ") parseLineForOspfArea(c) parseLineForOspfCounters(c) } return c.areas } func parseLineForOspfArea(c *ospfContext) { m := ospf.area.FindStringSubmatch(c.line) if m == nil { return } a := &protocol.OspfArea{Name: m[1]} c.current = a c.areas = append(c.areas, a) } func parseLineForOspfCounters(c *ospfContext) { if c.current == nil { return } m := ospf.counters.FindStringSubmatch(c.line) if m == nil { return } name := m[1] value := parseInt(m[2]) if name == "interfaces" { c.current.InterfaceCount = value } if name == "neighbors" { c.current.NeighborCount = value } if name == "adjacent neighbors" { c.current.NeighborAdjacentCount = value } } bird_exporter-1.2.2/parser/parser.go000066400000000000000000000133001333773000500175010ustar00rootroot00000000000000package parser import ( "bufio" "bytes" "fmt" "regexp" "strconv" "strings" "time" "github.com/czerwonk/bird_exporter/protocol" "github.com/prometheus/common/log" ) var ( protocolRegex *regexp.Regexp routeRegex *regexp.Regexp uptimeRegex *regexp.Regexp routeChangeRegex *regexp.Regexp channelRegex *regexp.Regexp ) type context struct { current *protocol.Protocol line string handled bool protocols []*protocol.Protocol ipVersion string } func init() { protocolRegex = regexp.MustCompile(`^(?:1002\-)?([^\s]+)\s+(BGP|OSPF|Direct|Device|Kernel|Static)\s+([^\s]+)\s+([^\s]+)\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|[^\s]+)(?:\s+(.*?))?$`) routeRegex = regexp.MustCompile(`^\s+Routes:\s+(\d+) imported, (?:(\d+) filtered, )?(\d+) exported(?:, (\d+) preferred)?`) uptimeRegex = regexp.MustCompile(`^(?:((\d+):(\d{2}):(\d{2}))|(\d+)|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}))$`) routeChangeRegex = regexp.MustCompile(`(Import|Export) (updates|withdraws):\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s*`) channelRegex = regexp.MustCompile(`Channel ipv(4|6)`) } // Parser parses bird output and returns protocol.Protocol structs func ParseProtocols(data []byte, ipVersion string) []*protocol.Protocol { reader := bytes.NewReader(data) scanner := bufio.NewScanner(reader) c := &context{protocols: make([]*protocol.Protocol, 0), ipVersion: ipVersion} var handlers = []func(*context){ handleEmptyLine, parseLineForProtocol, parseLineForChannel, parseLineForRoutes, parseLineForRouteChanges, } for scanner.Scan() { c.line = strings.TrimRight(scanner.Text(), " ") c.handled = false for _, h := range handlers { if !c.handled { h(c) } } } return c.protocols } func handleEmptyLine(c *context) { if c.line != "" { return } c.current = nil c.handled = true } func parseLineForProtocol(c *context) { match := protocolRegex.FindStringSubmatch(c.line) if match == nil { return } proto := parseProto(match[2]) ut := parseUptime(match[5]) c.current = protocol.NewProtocol(match[1], proto, c.ipVersion, ut) c.current.Up = parseState(match[4]) fillAttributes(c.current, match) c.protocols = append(c.protocols, c.current) c.handled = true } func parseProto(val string) int { switch val { case "BGP": return protocol.BGP case "OSPF": return protocol.OSPF case "Direct": return protocol.Direct case "Kernel": return protocol.Kernel case "Static": return protocol.Static } return protocol.PROTO_UNKNOWN } func parseState(state string) int { if state == "up" { return 1 } return 0 } func parseUptime(value string) int { match := uptimeRegex.FindStringSubmatch(value) if match == nil { return 0 } if len(match[1]) > 0 { return parseUptimeForDuration(match) } if len(match[5]) > 0 { return parseUptimeForTimestamp(value) } return parseUptimeForIso(value) } func parseUptimeForIso(s string) int { start, err := time.Parse("2006-01-02 15:04:05", s) if err != nil { log.Errorln(err) return 0 } return int(time.Since(start).Seconds()) } func parseUptimeForDuration(duration []string) int { h := parseInt(duration[2]) m := parseInt(duration[3]) s := parseInt(duration[4]) str := fmt.Sprintf("%dh%dm%ds", h, m, s) d, err := time.ParseDuration(str) if err != nil { log.Errorln(err) return 0 } return int(d.Seconds()) } func parseUptimeForTimestamp(timestamp string) int { since := parseInt(timestamp) s := time.Unix(since, 0) d := time.Since(s) return int(d.Seconds()) } func parseLineForChannel(c *context) { if c.ipVersion != "" || c.current == nil { return } channel := channelRegex.FindStringSubmatch(c.line) if channel == nil { return } if len(c.current.IpVersion) == 0 { c.current.IpVersion = channel[1] } else { c.current = &protocol.Protocol{ Name: c.current.Name, Proto: c.current.Proto, Up: c.current.Up, Uptime: c.current.Uptime, IpVersion: channel[1], } c.protocols = append(c.protocols, c.current) } c.handled = true } func parseLineForRoutes(c *context) { if c.current == nil { return } match := routeRegex.FindStringSubmatch(c.line) if match == nil { return } c.current.Imported, _ = strconv.ParseInt(match[1], 10, 64) c.current.Exported, _ = strconv.ParseInt(match[3], 10, 64) if len(match[2]) > 0 { c.current.Filtered, _ = strconv.ParseInt(match[2], 10, 64) } if len(match[4]) > 0 { c.current.Preferred, _ = strconv.ParseInt(match[4], 10, 64) } c.handled = true } func parseLineForRouteChanges(c *context) { if c.current == nil { return } match := routeChangeRegex.FindStringSubmatch(c.line) if match == nil { return } x := getRouteChangeCount(match, c.current) x.Received = parseRouteChangeValue(match[3]) x.Rejected = parseRouteChangeValue(match[4]) x.Filtered = parseRouteChangeValue(match[5]) x.Ignored = parseRouteChangeValue(match[6]) x.Accepted = parseRouteChangeValue(match[7]) c.handled = true } func getRouteChangeCount(values []string, p *protocol.Protocol) *protocol.RouteChangeCount { if values[1] == "Import" { if values[2] == "updates" { return &p.ImportUpdates } return &p.ImportWithdraws } else { if values[2] == "updates" { return &p.ExportUpdates } return &p.ExportWithdraws } } func parseRouteChangeValue(value string) int64 { if value == "---" { return 0 } return parseInt(value) } func parseInt(value string) int64 { i, err := strconv.ParseInt(value, 10, 64) if err != nil { log.Errorln(err) return 0 } return i } func fillAttributes(p *protocol.Protocol, m []string) { if p.Proto == protocol.OSPF { p.Attributes["running"] = float64(parseOspfRunning(m[6])) } } func parseOspfRunning(state string) int { if state == "Running" { return 1 } return 0 } bird_exporter-1.2.2/parser/parser_test.go000066400000000000000000000423411333773000500205470ustar00rootroot00000000000000package parser import ( "testing" "time" "github.com/czerwonk/bird_exporter/protocol" "github.com/czerwonk/testutils/assert" ) func TestEstablishedBgpOldTimeFormat(t *testing.T) { data := "foo BGP master up 1514768400 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx" s := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC) min := int(time.Since(s).Seconds()) p := ParseProtocols([]byte(data), "4") max := int(time.Since(s).Seconds()) x := p[0] assert.StringEqual("name", "foo", x.Name, t) assert.IntEqual("proto", protocol.BGP, x.Proto, t) assert.IntEqual("established", 1, x.Up, t) assert.Int64Equal("imported", 12, x.Imported, t) assert.Int64Equal("exported", 34, x.Exported, t) assert.Int64Equal("filtered", 1, x.Filtered, t) assert.Int64Equal("preferred", 100, x.Preferred, t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) assert.That("uptime", "uptime is feasable", func() bool { return x.Uptime >= min && max <= x.Uptime }, t) } func TestEstablishedBgpCurrentTimeFormat(t *testing.T) { data := "foo BGP master up 00:01:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "foo", x.Name, t) assert.IntEqual("proto", protocol.BGP, x.Proto, t) assert.IntEqual("established", 1, x.Up, t) assert.Int64Equal("imported", 12, x.Imported, t) assert.Int64Equal("exported", 34, x.Exported, t) assert.Int64Equal("filtered", 1, x.Filtered, t) assert.Int64Equal("preferred", 100, x.Preferred, t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) assert.IntEqual("uptime", 60, x.Uptime, t) } func TestEstablishedBgpIsoLongTimeFormat(t *testing.T) { data := "foo BGP master up 2018-01-01 01:00:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx" s := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC) min := int(time.Since(s).Seconds()) p := ParseProtocols([]byte(data), "4") max := int(time.Since(s).Seconds()) assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "foo", x.Name, t) assert.IntEqual("proto", protocol.BGP, x.Proto, t) assert.IntEqual("established", 1, x.Up, t) assert.Int64Equal("imported", 12, x.Imported, t) assert.Int64Equal("exported", 34, x.Exported, t) assert.Int64Equal("filtered", 1, x.Filtered, t) assert.Int64Equal("preferred", 100, x.Preferred, t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) assert.That("uptime", "uptime is feasable", func() bool { return x.Uptime >= min && max <= x.Uptime }, t) } func TestIpv6Bgp(t *testing.T) { data := "foo BGP master up 00:01:00 Established\ntest\nbar\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "6") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("ipVersion", "6", x.IpVersion, t) } func TestActiveBgp(t *testing.T) { data := "bar BGP master start 2016-01-01 Active\ntest\nbar" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "bar", x.Name, t) assert.IntEqual("proto", protocol.BGP, x.Proto, t) assert.IntEqual("established", 0, x.Up, t) assert.IntEqual("imported", 0, int(x.Imported), t) assert.IntEqual("exported", 0, int(x.Exported), t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) assert.IntEqual("uptime", 0, int(x.Uptime), t) } func Test2BgpSessions(t *testing.T) { data := "foo BGP master up 00:01:00 Established\ntest\n Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\nbar BGP master start 2016-01-01 Active\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 2, len(p), t) } func TestUpdateAndWithdrawCounts(t *testing.T) { data := "foo BGP master up 00:01:00 Established\ntest\n" + " Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\n" + " Route change stats: received rejected filtered ignored accepted\n" + " Import updates: 1 2 3 4 5\n" + " Import withdraws: 6 7 8 9 10\n" + " Export updates: 11 12 13 14 15\n" + " Export withdraws: 16 17 18 19 ---" p := ParseProtocols([]byte(data), "4") x := p[0] assert.Int64Equal("import updates received", 1, x.ImportUpdates.Received, t) assert.Int64Equal("import updates rejected", 2, x.ImportUpdates.Rejected, t) assert.Int64Equal("import updates filtered", 3, x.ImportUpdates.Filtered, t) assert.Int64Equal("import updates ignored", 4, x.ImportUpdates.Ignored, t) assert.Int64Equal("import updates accepted", 5, x.ImportUpdates.Accepted, t) assert.Int64Equal("import withdraws received", 6, x.ImportWithdraws.Received, t) assert.Int64Equal("import withdraws rejected", 7, x.ImportWithdraws.Rejected, t) assert.Int64Equal("import withdraws filtered", 8, x.ImportWithdraws.Filtered, t) assert.Int64Equal("import withdraws ignored", 9, x.ImportWithdraws.Ignored, t) assert.Int64Equal("import withdraws accepted", 10, x.ImportWithdraws.Accepted, t) assert.Int64Equal("export updates received", 11, x.ExportUpdates.Received, t) assert.Int64Equal("export updates rejected", 12, x.ExportUpdates.Rejected, t) assert.Int64Equal("export updates filtered", 13, x.ExportUpdates.Filtered, t) assert.Int64Equal("export updates ignored", 14, x.ExportUpdates.Ignored, t) assert.Int64Equal("export updates accepted", 15, x.ExportUpdates.Accepted, t) assert.Int64Equal("export withdraws received", 16, x.ExportWithdraws.Received, t) assert.Int64Equal("export withdraws rejected", 17, x.ExportWithdraws.Rejected, t) assert.Int64Equal("export withdraws filtered", 18, x.ExportWithdraws.Filtered, t) assert.Int64Equal("export withdraws ignored", 19, x.ExportWithdraws.Ignored, t) assert.Int64Equal("export withdraws accepted", 0, x.ExportWithdraws.Accepted, t) } func TestWithBird2(t *testing.T) { data := "Name Proto Table State Since Info\n" + "bgp1 BGP master up 1494926415\n" + " Channel ipv6\n" + " Routes: 1 imported, 2 filtered, 3 exported, 4 preferred\n" + "\n" + "direct1 Direct --- up 1513027903\n" + " Channel ipv4\n" + " State: UP\n" + " Table: master4\n" + " Preference: 240\n" + " Input filter: ACCEPT\n" + " Output filter: REJECT\n" + " Routes: 12 imported, 1 filtered, 34 exported, 100 preferred\n" + " Route change stats: received rejected filtered ignored accepted\n" + " Import updates: 1 2 3 4 5\n" + " Import withdraws: 6 7 8 9 10\n" + " Export updates: 11 12 13 14 15\n" + " Export withdraws: 16 17 18 19 ---\n" + " Channel ipv6\n" + " State: UP\n" + " Table: master6\n" + " Preference: 240\n" + " Input filter: ACCEPT\n" + " Output filter: REJECT\n" + " Routes: 3 imported, 7 filtered, 5 exported, 13 preferred\n" + " Route change stats: received rejected filtered ignored accepted\n" + " Import updates: 20 21 22 23 24\n" + " Import withdraws: 25 26 27 28 29\n" + " Export updates: 30 31 32 33 34\n" + " Export withdraws: 35 36 37 38 ---\n" + "\n" + "ospf1 OSPF master up 1494926415\n" + " Channel ipv4\n" + " Routes: 4 imported, 3 filtered, 2 exported, 1 preferred\n" + "\n" p := ParseProtocols([]byte(data), "") assert.IntEqual("protocols", 4, len(p), t) x := p[0] assert.StringEqual("BGP ipv6 name", "bgp1", x.Name, t) assert.IntEqual("BGP ipv6 proto", protocol.BGP, x.Proto, t) assert.StringEqual("BGP ipv6 ip version", "6", x.IpVersion, t) assert.Int64Equal("BGP ipv6 imported", 1, x.Imported, t) assert.Int64Equal("BGP ipv6 exported", 3, x.Exported, t) assert.Int64Equal("BGP ipv6 filtered", 2, x.Filtered, t) assert.Int64Equal("BGP ipv6 preferred", 4, x.Preferred, t) x = p[1] assert.StringEqual("BGP ipv4 name", "direct1", x.Name, t) assert.IntEqual("Direct ipv4 proto", protocol.Direct, x.Proto, t) assert.StringEqual("Direct ipv4 ip version", "4", x.IpVersion, t) assert.Int64Equal("Direct ipv4 imported", 12, x.Imported, t) assert.Int64Equal("Direct ipv4 exported", 34, x.Exported, t) assert.Int64Equal("Direct ipv4 filtered", 1, x.Filtered, t) assert.Int64Equal("Direct ipv4 preferred", 100, x.Preferred, t) assert.Int64Equal("Direct ipv4 import updates received", 1, x.ImportUpdates.Received, t) assert.Int64Equal("Direct ipv4 import updates rejected", 2, x.ImportUpdates.Rejected, t) assert.Int64Equal("Direct ipv4 import updates filtered", 3, x.ImportUpdates.Filtered, t) assert.Int64Equal("Direct ipv4 import updates ignored", 4, x.ImportUpdates.Ignored, t) assert.Int64Equal("Direct ipv4 import updates accepted", 5, x.ImportUpdates.Accepted, t) assert.Int64Equal("Direct ipv4 import withdraws received", 6, x.ImportWithdraws.Received, t) assert.Int64Equal("Direct ipv4 import withdraws rejected", 7, x.ImportWithdraws.Rejected, t) assert.Int64Equal("Direct ipv4 import withdraws filtered", 8, x.ImportWithdraws.Filtered, t) assert.Int64Equal("Direct ipv4 import withdraws ignored", 9, x.ImportWithdraws.Ignored, t) assert.Int64Equal("Direct ipv4 import withdraws accepted", 10, x.ImportWithdraws.Accepted, t) assert.Int64Equal("Direct ipv4 export updates received", 11, x.ExportUpdates.Received, t) assert.Int64Equal("Direct ipv4 export updates rejected", 12, x.ExportUpdates.Rejected, t) assert.Int64Equal("Direct ipv4 export updates filtered", 13, x.ExportUpdates.Filtered, t) assert.Int64Equal("Direct ipv4 export updates ignored", 14, x.ExportUpdates.Ignored, t) assert.Int64Equal("Direct ipv4 export updates accepted", 15, x.ExportUpdates.Accepted, t) assert.Int64Equal("Direct ipv4 export withdraws received", 16, x.ExportWithdraws.Received, t) assert.Int64Equal("Direct ipv4 export withdraws rejected", 17, x.ExportWithdraws.Rejected, t) assert.Int64Equal("Direct ipv4 export withdraws filtered", 18, x.ExportWithdraws.Filtered, t) assert.Int64Equal("Direct ipv4 export withdraws ignored", 19, x.ExportWithdraws.Ignored, t) assert.Int64Equal("Direct ipv4 export withdraws accepted", 0, x.ExportWithdraws.Accepted, t) x = p[2] assert.StringEqual("BGP ipv4 name", "direct1", x.Name, t) assert.IntEqual("Direct ipv6 proto", protocol.Direct, x.Proto, t) assert.StringEqual("Direct ipv6 ip version", "6", x.IpVersion, t) assert.Int64Equal("Direct ipv6 imported", 3, x.Imported, t) assert.Int64Equal("Direct ipv6 exported", 5, x.Exported, t) assert.Int64Equal("Direct ipv6 filtered", 7, x.Filtered, t) assert.Int64Equal("Direct ipv6 preferred", 13, x.Preferred, t) assert.Int64Equal("Direct ipv6 import updates received", 20, x.ImportUpdates.Received, t) assert.Int64Equal("Direct ipv6 import updates rejected", 21, x.ImportUpdates.Rejected, t) assert.Int64Equal("Direct ipv6 import updates filtered", 22, x.ImportUpdates.Filtered, t) assert.Int64Equal("Direct ipv6 import updates ignored", 23, x.ImportUpdates.Ignored, t) assert.Int64Equal("Direct ipv6 import updates accepted", 24, x.ImportUpdates.Accepted, t) assert.Int64Equal("Direct ipv6 import withdraws received", 25, x.ImportWithdraws.Received, t) assert.Int64Equal("Direct ipv6 import withdraws rejected", 26, x.ImportWithdraws.Rejected, t) assert.Int64Equal("Direct ipv6 import withdraws filtered", 27, x.ImportWithdraws.Filtered, t) assert.Int64Equal("Direct ipv6 import withdraws ignored", 28, x.ImportWithdraws.Ignored, t) assert.Int64Equal("Direct ipv6 import withdraws accepted", 29, x.ImportWithdraws.Accepted, t) assert.Int64Equal("Direct ipv6 export updates received", 30, x.ExportUpdates.Received, t) assert.Int64Equal("Direct ipv6 export updates rejected", 31, x.ExportUpdates.Rejected, t) assert.Int64Equal("Direct ipv6 export updates filtered", 32, x.ExportUpdates.Filtered, t) assert.Int64Equal("Direct ipv6 export updates ignored", 33, x.ExportUpdates.Ignored, t) assert.Int64Equal("Direct ipv6 export updates accepted", 34, x.ExportUpdates.Accepted, t) assert.Int64Equal("Direct ipv6 export withdraws received", 35, x.ExportWithdraws.Received, t) assert.Int64Equal("Direct ipv6 export withdraws rejected", 36, x.ExportWithdraws.Rejected, t) assert.Int64Equal("Direct ipv6 export withdraws filtered", 37, x.ExportWithdraws.Filtered, t) assert.Int64Equal("Direct ipv6 export withdraws ignored", 38, x.ExportWithdraws.Ignored, t) assert.Int64Equal("Direct ipv6 export withdraws accepted", 0, x.ExportWithdraws.Accepted, t) x = p[3] assert.StringEqual("OSPF ipv4 name", "ospf1", x.Name, t) assert.IntEqual("OSPF ipv4 proto", protocol.OSPF, x.Proto, t) assert.StringEqual("OSPF ipv4 ip version", "4", x.IpVersion, t) assert.Int64Equal("OSPF ipv4 imported", 4, x.Imported, t) assert.Int64Equal("OSPF ipv4 exported", 2, x.Exported, t) assert.Int64Equal("OSPF ipv4 filtered", 3, x.Filtered, t) assert.Int64Equal("OSPF ipv4 preferred", 1, x.Preferred, t) } func TestOspfOldTimeFormat(t *testing.T) { data := "ospf1 OSPF master up 1481973060 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "ospf1", x.Name, t) assert.IntEqual("proto", protocol.OSPF, x.Proto, t) assert.IntEqual("up", 1, x.Up, t) assert.Int64Equal("imported", 12, x.Imported, t) assert.Int64Equal("exported", 34, x.Exported, t) assert.Int64Equal("preferred", 100, x.Preferred, t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) } func TestOspfCurrentTimeFormat(t *testing.T) { data := "ospf1 OSPF master up 00:01:00 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "ospf1", x.Name, t) assert.IntEqual("proto", protocol.OSPF, x.Proto, t) assert.IntEqual("up", 1, x.Up, t) assert.Int64Equal("imported", 12, x.Imported, t) assert.Int64Equal("exported", 34, x.Exported, t) assert.Int64Equal("preferred", 100, x.Preferred, t) assert.StringEqual("ipVersion", "4", x.IpVersion, t) assert.IntEqual("uptime", 60, x.Uptime, t) } func TestOspfProtocolDown(t *testing.T) { data := "o_hrz OSPF t_hrz down 1494926415 \n Preference: 150\n Input filter: ACCEPT\n Output filter: REJECT\nxxx" p := ParseProtocols([]byte(data), "6") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.StringEqual("name", "o_hrz", x.Name, t) assert.IntEqual("proto", protocol.OSPF, x.Proto, t) assert.IntEqual("up", 0, x.Up, t) assert.Int64Equal("imported", 0, x.Imported, t) assert.Int64Equal("exported", 0, x.Exported, t) assert.StringEqual("ipVersion", "6", x.IpVersion, t) } func TestOspfRunning(t *testing.T) { data := "ospf1 OSPF master up 00:01:00 Running\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.Float64Equal("running", 1, x.Attributes["running"], t) } func TestOspfAlone(t *testing.T) { data := "ospf1 OSPF master up 00:01:00 Alone\ntest\nbar\n Routes: 12 imported, 34 exported, 100 preferred\nxxx" p := ParseProtocols([]byte(data), "4") assert.IntEqual("protocols", 1, len(p), t) x := p[0] assert.Float64Equal("running", 0, x.Attributes["running"], t) } func TestOspfArea(t *testing.T) { data := "ospf1:\n" + "RFC1583 compatibility: disabled\n" + "Stub router: No\n" + "RT scheduler tick: 1\n" + "Number of areas: 2\n" + "Number of LSAs in DB: 33\n" + " Area: 0.0.0.0 (0) [BACKBONE]\n" + " Stub: No\n" + " NSSA: No\n" + " Transit: No\n" + " Number of interfaces: 3\n" + " Number of neighbors: 2\n" + " Number of adjacent neighbors: 1\n" + " Area: 0.0.0.1 (1)\n" + " Stub: No\n" + " NSSA: No\n" + " Transit: No\n" + " Number of interfaces: 4\n" + " Number of neighbors: 6\n" + " Number of adjacent neighbors: 5\n" a := ParseOspf([]byte(data)) assert.IntEqual("areas", 2, len(a), t) a1 := a[0] assert.StringEqual("Area1 Name", "0", a1.Name, t) assert.Int64Equal("Area1 InterfaceCount", 3, a1.InterfaceCount, t) assert.Int64Equal("Area1 NeighborCount", 2, a1.NeighborCount, t) assert.Int64Equal("Area1 NeighborAdjacentCount", 1, a1.NeighborAdjacentCount, t) a2 := a[1] assert.StringEqual("Area2 Name", "1", a2.Name, t) assert.Int64Equal("Area2 InterfaceCount", 4, a2.InterfaceCount, t) assert.Int64Equal("Area2 NeighborCount", 6, a2.NeighborCount, t) assert.Int64Equal("Area2 NeighborAdjacentCount", 5, a2.NeighborAdjacentCount, t) } bird_exporter-1.2.2/protocol/000077500000000000000000000000001333773000500162265ustar00rootroot00000000000000bird_exporter-1.2.2/protocol/ospf_area.go000066400000000000000000000002401333773000500205100ustar00rootroot00000000000000package protocol type OspfArea struct { Name string InterfaceCount int64 NeighborCount int64 NeighborAdjacentCount int64 } bird_exporter-1.2.2/protocol/protocol.go000066400000000000000000000015401333773000500204160ustar00rootroot00000000000000package protocol const ( PROTO_UNKNOWN = 0 BGP = 1 OSPF = 2 Kernel = 4 Static = 8 Direct = 16 ) type Protocol struct { Name string IpVersion string Proto int Up int Imported int64 Exported int64 Filtered int64 Preferred int64 Uptime int Attributes map[string]float64 ImportUpdates RouteChangeCount ImportWithdraws RouteChangeCount ExportUpdates RouteChangeCount ExportWithdraws RouteChangeCount } type RouteChangeCount struct { Received int64 Rejected int64 Filtered int64 Ignored int64 Accepted int64 } func NewProtocol(name string, proto int, ipVersion string, uptime int) *Protocol { return &Protocol{Name: name, Proto: proto, IpVersion: ipVersion, Uptime: uptime, Attributes: make(map[string]float64)} } bird_exporter-1.2.2/vendor/000077500000000000000000000000001333773000500156625ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/000077500000000000000000000000001333773000500177215ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/czerwonk/000077500000000000000000000000001333773000500215635ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/000077500000000000000000000000001333773000500240535ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/.gitignore000066400000000000000000000004401333773000500260410ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea *.iml bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/.travis.yml000066400000000000000000000000151333773000500261600ustar00rootroot00000000000000language: go bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/LICENSE000066400000000000000000000020601333773000500250560ustar00rootroot00000000000000MIT License Copyright (c) 2017 Daniel Czerwonk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/README.md000066400000000000000000000013341333773000500253330ustar00rootroot00000000000000# bird_socket [![GoDoc](https://godoc.org/github.com/czerwonk/bird_socket?status.svg)](https://godoc.org/github.com/czerwonk/bird_socket) [![Build Status](https://travis-ci.org/czerwonk/bird_socket.svg)](https://travis-ci.org/czerwonk/bird_socket) [![Go Report Card](https://goreportcard.com/badge/github.com/czerwonk/bird_socket)](https://goreportcard.com/report/github.com/czerwonk/bird_socket) [![Sourcegraph](https://sourcegraph.com/github.com/czerwonk/bird_socket/-/badge.svg)](https://sourcegraph.com/github.com/czerwonk/bird_socket?badge) Golang library to communicate with Bird routing daemon ## License (c) Daniel Czerwonk, 2017. Licensed under [MIT](LICENSE) license. ## Bird routing daemon see http://bird.network.cz/ bird_exporter-1.2.2/vendor/github.com/czerwonk/bird_socket/bird_socket.go000066400000000000000000000036351333773000500267010ustar00rootroot00000000000000package birdsocket import ( "net" "regexp" "strings" ) var birdReturnCodeRegex *regexp.Regexp func init() { birdReturnCodeRegex = regexp.MustCompile("\\d{4} \n$") } // BirdSocket encapsulates communication with Bird routing daemon type BirdSocket struct { SocketPath string BufferSize int Timeout int conn net.Conn } // NewSocket creates a new socket func NewSocket(socketPath string) *BirdSocket { return &BirdSocket{SocketPath: socketPath, BufferSize: 4096, Timeout: 30} } // Query sends an ad hoc query to Bird and waits for the reply func Query(socketPath, qry string) ([]byte, error) { s := NewSocket(socketPath) _, err := s.Connect() if err != nil { return nil, err } defer s.Close() return s.Query(qry) } // Connect connects to the Bird unix socket func (s *BirdSocket) Connect() ([]byte, error) { var err error s.conn, err = net.Dial("unix", s.SocketPath) if err != nil { return nil, err } buf := make([]byte, s.BufferSize) n, err := s.conn.Read(buf[:]) if err != nil { return nil, err } return buf[:n], err } // Close closes the connection to the socket func (s *BirdSocket) Close() { if s.conn != nil { s.conn.Close() } } // Query sends an query to Bird and waits for the reply func (s *BirdSocket) Query(qry string) ([]byte, error) { _, err := s.conn.Write([]byte(strings.Trim(qry, "\n") + "\n")) if err != nil { return nil, err } output, err := s.readFromSocket(s.conn) if err != nil { return nil, err } return output, nil } func (s *BirdSocket) readFromSocket(conn net.Conn) ([]byte, error) { b := make([]byte, 0) buf := make([]byte, s.BufferSize) done := false for !done { n, err := conn.Read(buf[:]) if err != nil { return nil, err } b = append(b, buf[:n]...) done = endsWithBirdReturnCode(b) } return b, nil } func endsWithBirdReturnCode(b []byte) bool { if len(b) < 6 { return false } return birdReturnCodeRegex.Match(b[len(b)-6:]) } bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/000077500000000000000000000000001333773000500236235ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/.travis.yml000066400000000000000000000000151333773000500257300ustar00rootroot00000000000000language: go bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/LICENSE000066400000000000000000000020601333773000500246260ustar00rootroot00000000000000MIT License Copyright (c) 2016 Daniel Czerwonk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/README.md000066400000000000000000000006121333773000500251010ustar00rootroot00000000000000# testutils [![Build Status](https://travis-ci.org/czerwonk/testutils.svg)][travis] Simple collection of assertion functions to prevent boilerplate code ## Remark this collection will grow in the future ## Install ``` go get -u github.com/czerwonk/testutils ``` ## License (c) Daniel Czerwonk, 2016. Licensed under [MIT](LICENSE) license. [travis]: https://travis-ci.org/czerwonk/testutils bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/assert/000077500000000000000000000000001333773000500251245ustar00rootroot00000000000000bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/assert/assert.go000066400000000000000000000034651333773000500267640ustar00rootroot00000000000000package assert import "testing" const equalError string = "%s: expected %v but got %v" type Assertion func() bool func StringEqual(name string, expected, current string, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func IntEqual(name string, expected, current int, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func ByteEqual(name string, expected, current byte, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Int32Equal(name string, expected, current int32, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Int64Equal(name string, expected, current int64, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Float32Equal(name string, expected, current float32, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Float64Equal(name string, expected, current float64, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Complex64Equal(name string, expected, current complex64, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func Complex128Equal(name string, expected, current complex128, t *testing.T) { if current != expected { t.Fatalf(equalError, name, expected, current) } } func True(name string, current bool, t *testing.T) { if !current { t.Fatalf(equalError, name, true, false) } } func False(name string, current bool, t *testing.T) { if current { t.Fatalf(equalError, name, false, true) } } func That(name, text string, assertion Assertion, t *testing.T) { if !assertion() { t.Fatalf("%s: %s", name, text) } } bird_exporter-1.2.2/vendor/github.com/czerwonk/testutils/assert/assert_test.go000066400000000000000000000023361333773000500300170ustar00rootroot00000000000000package assert import "testing" func TestStringEqual(t *testing.T) { var x string = "x" var y string = "x" StringEqual("test", x, y, t) } func TestIntEqual(t *testing.T) { var x int = 123 var y int = 123 IntEqual("test", x, y, t) } func TestByteEqual(t *testing.T) { var x int = 123 var y int = 123 IntEqual("test", x, y, t) } func TestInt32Equal(t *testing.T) { var x int32 = 123 var y int32 = 123 Int32Equal("test", x, y, t) } func TestInt64Equal(t *testing.T) { var x int64 = 123 var y int64 = 123 Int64Equal("test", x, y, t) } func TestFloat32Equal(t *testing.T) { var x float32 = 1.23 var y float32 = 1.23 Float32Equal("test", x, y, t) } func TestFloat64Equal(t *testing.T) { var x float64 = 1.23 var y float64 = 1.23 Float64Equal("test", x, y, t) } func TestComplex64Equal(t *testing.T) { var x complex64 = 1.23 var y complex64 = 1.23 Complex64Equal("test", x, y, t) } func TestComplex128Equal(t *testing.T) { var x complex128 = 1.23 var y complex128 = 1.23 Complex128Equal("test", x, y, t) } func TestTrue(t *testing.T) { True("test", true, t) } func TestFalse(t *testing.T) { False("test", false, t) } func TestThat(t *testing.T) { That("test", "foo", func() bool { return true }, t) }