├── .gitlab-ci.yml ├── COPYING ├── Dockerfile ├── README.md └── netbox-prometheus-sd.py /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:latest 2 | 3 | # When using dind, it's wise to use the overlayfs driver for 4 | # improved performance. 5 | variables: 6 | DOCKER_DRIVER: overlay2 7 | 8 | services: 9 | - docker:dind 10 | 11 | before_script: 12 | - docker info 13 | 14 | build: 15 | stage: build 16 | script: 17 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 18 | - docker build -t $CI_REGISTRY_IMAGE . 19 | - docker push $CI_REGISTRY_IMAGE:latest 20 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2018 © Enix SAS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | The Software is provided "as is", without warranty of any kind, express or 14 | implied, including but not limited to the warranties of merchantability, 15 | fitness for a particular purpose and noninfringement. In no event shall the 16 | authors or copyright holders be liable for any claim, damages or other 17 | liability, whether in an action of contract, tort or otherwise, arising from, 18 | out of or in connection with the software or the use or other dealings in the 19 | Software. 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | MAINTAINER Antoine Millet 3 | 4 | RUN apt update && apt install -y python3-pip 5 | RUN pip3 install pynetbox netaddr 6 | COPY netbox-prometheus-sd.py /bin/netbox-prometheus-sd 7 | RUN chmod +x /bin/netbox-prometheus-sd 8 | RUN mkdir /output 9 | 10 | CMD while true; do (/bin/netbox-prometheus-sd "$NETBOX_URL" "$NETBOX_TOKEN" "/output/${OUTPUT_FILE-netbox.json}"; sleep $INTERVAL); done 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Service discovery for [Prometheus](https://prometheus.io/) using devices from [Netbox](https://github.com/digitalocean/netbox). 2 | 3 | # Legal 4 | 5 | This project is released under MIT license, copyright 2018 ENIX SAS. 6 | 7 | # Contribute 8 | 9 | Feel free to contribute to this project through Github pull-requests. We also 10 | accept well formatted git patches sent by email to the maintainer. 11 | 12 | Current Maintainer: Antoine Millet 13 | 14 | # Requirement 15 | 16 | This project requires Python 3 and [Pynetbox](https://github.com/digitalocean/pynetbox/). 17 | 18 | Also, this project requires a custom field to be created into your Netbox instance. 19 | By default, this custom field is named `prom_labels` and can be created in the 20 | Netbox admin page (Home -> Extras -> Custom fields) with the following settings: 21 | 22 | - Objects: select `dcim > device` and `virtualization > virtual machine` 23 | - Type: `Text` 24 | - Name: `prom_labels` 25 | 26 | # Usage 27 | 28 | ``` 29 | usage: netbox-prometheus-sd.py [-h] [-p PORT] [-f CUSTOM_FIELD] 30 | url token output 31 | 32 | positional arguments: 33 | url URL to Netbox 34 | token Authentication Token 35 | output Output file 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | -p PORT, --port PORT Default target port; Can be overridden using the 40 | __port__ label 41 | -f CUSTOM_FIELD, --custom-field CUSTOM_FIELD 42 | Netbox custom field to use to get the target labels 43 | ``` 44 | 45 | The service discovery script requires the URL to the Netbox instance, an 46 | API token that can be generated into the user profile page of Netbox and a path 47 | to an output file. 48 | 49 | Optionally, you can customize the custom field used to get target labels in Netbox 50 | using the `--custom-field` option. You can also customize the default port on which 51 | the target will point to using the `--port` option. Note that this port can be customized 52 | per target using the `__port__` label set in the custom field. 53 | 54 | The output will be generated in the file pointed by the `output` argument. 55 | 56 | In the Prometheus configuration, declare a new scrape job using the file_sd_configs 57 | service discovery: 58 | 59 | ``` 60 | - job_name: 'netbox' 61 | file_sd_configs: 62 | - files: 63 | - '/path/to/my/output.json' 64 | ``` 65 | -------------------------------------------------------------------------------- /netbox-prometheus-sd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import json 6 | import argparse 7 | import itertools 8 | import netaddr 9 | 10 | import pynetbox 11 | 12 | 13 | def main(args): 14 | targets = [] 15 | netbox = pynetbox.api(args.url, token=args.token) 16 | 17 | # Filter out devices without primary IP address as it is a requirement 18 | # to be polled by Prometheus 19 | devices = netbox.dcim.devices.filter(has_primary_ip=True) 20 | vm = netbox.virtualization.virtual_machines.filter(has_primary_ip=True) 21 | ips = netbox.ipam.ip_addresses.filter(**{'cf_%s' % args.custom_field: '{'}) 22 | 23 | for device in itertools.chain(devices, vm, ips): 24 | if device.custom_fields.get(args.custom_field): 25 | labels = {'__port__': str(args.port)} 26 | if getattr(device, 'name', None): 27 | labels['__meta_netbox_name'] = device.name 28 | else: 29 | labels['__meta_netbox_name'] = repr(device) 30 | if device.tenant: 31 | labels['__meta_netbox_tenant'] = device.tenant.slug 32 | if device.tenant.group: 33 | labels['__meta_netbox_tenant_group'] = device.tenant.group.slug 34 | if getattr(device, 'cluster', None): 35 | labels['__meta_netbox_cluster'] = device.cluster.name 36 | if getattr(device, 'asset_tag', None): 37 | labels['__meta_netbox_asset_tag'] = device.asset_tag 38 | if getattr(device, 'device_role', None): 39 | labels['__meta_netbox_role'] = device.device_role.slug 40 | if getattr(device, 'device_type', None): 41 | labels['__meta_netbox_type'] = device.device_type.model 42 | if getattr(device, 'rack', None): 43 | labels['__meta_netbox_rack'] = device.rack.name 44 | if getattr(device, 'site', None): 45 | labels['__meta_netbox_pop'] = device.site.slug 46 | if getattr(device, 'serial', None): 47 | labels['__meta_netbox_serial'] = device.serial 48 | if getattr(device, 'parent_device', None): 49 | labels['__meta_netbox_parent'] = device.parent_device.name 50 | if getattr(device, 'address', None): 51 | labels['__meta_netbox_address'] = device.address 52 | if getattr(device, 'description', None): 53 | labels['__meta_netbox_description'] = device.description 54 | try: 55 | device_targets = json.loads(device.custom_fields[args.custom_field]) 56 | except ValueError: 57 | continue # Ignore errors while decoding the target json FIXME: logging 58 | 59 | if not isinstance(device_targets, list): 60 | device_targets = [device_targets] 61 | 62 | for target in device_targets: 63 | target_labels = labels.copy() 64 | target_labels.update(target) 65 | if hasattr(device, 'primary_ip'): 66 | address = device.primary_ip 67 | else: 68 | address = device 69 | targets.append({'targets': ['%s:%s' % (str(netaddr.IPNetwork(address.address).ip), 70 | target_labels['__port__'])], 71 | 'labels': target_labels}) 72 | 73 | temp_file = None 74 | if args.output == '-': 75 | output = sys.stdout 76 | else: 77 | temp_file = '{}.tmp'.format(args.output) 78 | output = open(temp_file, 'w') 79 | 80 | json.dump(targets, output, indent=4) 81 | output.write('\n') 82 | 83 | if temp_file: 84 | output.close() 85 | os.rename(temp_file, args.output) 86 | else: 87 | output.flush() 88 | 89 | 90 | if __name__ == '__main__': 91 | parser = argparse.ArgumentParser() 92 | parser.add_argument('-p', '--port', default=10000, 93 | help='Default target port; Can be overridden using the __port__ label') 94 | parser.add_argument('-f', '--custom-field', default='prom_labels', 95 | help='Netbox custom field to use to get the target labels') 96 | parser.add_argument('url', help='URL to Netbox') 97 | parser.add_argument('token', help='Authentication Token') 98 | parser.add_argument('output', help='Output file') 99 | 100 | args = parser.parse_args() 101 | main(args) 102 | --------------------------------------------------------------------------------