mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Add a netbox dynamic inventory plugin (#45347)
This commit is contained in:
parent
00d04ef757
commit
66f03827d6
2 changed files with 355 additions and 1 deletions
3
.github/BOTMETA.yml
vendored
3
.github/BOTMETA.yml
vendored
|
@ -855,8 +855,9 @@ files:
|
|||
- inventory
|
||||
labels:
|
||||
- cloud
|
||||
lib/ansible/plugins/inventory/vultr.py: *vultr
|
||||
lib/ansible/plugins/inventory/netbox.py: sieben
|
||||
lib/ansible/plugins/inventory/scaleway.py: *scaleway
|
||||
lib/ansible/plugins/inventory/vultr.py: *vultr
|
||||
lib/ansible/plugins/inventory/yaml.py:
|
||||
support: core
|
||||
###############################
|
||||
|
|
353
lib/ansible/plugins/inventory/netbox.py
Normal file
353
lib/ansible/plugins/inventory/netbox.py
Normal file
|
@ -0,0 +1,353 @@
|
|||
# Copyright (c) 2018 Remy Leone
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: netbox
|
||||
plugin_type: inventory
|
||||
author:
|
||||
- Remy Leone (@sieben)
|
||||
short_description: NetBox inventory source
|
||||
description:
|
||||
- Get inventory hosts from NetBox
|
||||
options:
|
||||
plugin:
|
||||
description: token that ensures this is a source file for the 'netbox' plugin.
|
||||
required: True
|
||||
choices: ['netbox']
|
||||
api_endpoint:
|
||||
description: Endpoint of the NetBox API
|
||||
required: True
|
||||
env:
|
||||
- name: NETBOX_API
|
||||
validate_certs:
|
||||
description:
|
||||
- Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted.
|
||||
default: True
|
||||
type: boolean
|
||||
token:
|
||||
required: True
|
||||
description: NetBox token.
|
||||
env:
|
||||
# in order of precedence
|
||||
- name: NETBOX_TOKEN
|
||||
- name: NETBOX_API_KEY
|
||||
group_by:
|
||||
description: Keys used to create groups.
|
||||
type: list
|
||||
choices:
|
||||
- sites
|
||||
- tenants
|
||||
- racks
|
||||
- device_roles
|
||||
- device_types
|
||||
- manufacturers
|
||||
default: []
|
||||
query_filters:
|
||||
description: List of parameters passed to the query string (Multiple values may be separated by commas)
|
||||
type: list
|
||||
timeout:
|
||||
description: Timeout for Netbox requests in seconds
|
||||
type: int
|
||||
default: 60
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# netbox_inventory.yml file in YAML format
|
||||
# Example command line: ansible-inventory --list -i netbox_inventory.yml
|
||||
|
||||
plugin: netbox
|
||||
api_endpoint: http://localhost:8000
|
||||
group_by:
|
||||
- device_roles
|
||||
query_filters:
|
||||
- role: network-edge-router
|
||||
'''
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from sys import version as python_version
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin
|
||||
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import urljoin, urlencode
|
||||
from ansible.module_utils.compat.ipaddress import ip_interface
|
||||
|
||||
|
||||
ALLOWED_DEVICE_QUERY_PARAMETERS = (
|
||||
"asset_tag",
|
||||
"cluster_id",
|
||||
"device_type_id",
|
||||
"has_primary_ip",
|
||||
"is_console_server",
|
||||
"is_full_depth",
|
||||
"is_network_device",
|
||||
"is_pdu",
|
||||
"mac_address",
|
||||
"manufacturer",
|
||||
"manufacturer_id",
|
||||
"model",
|
||||
"name",
|
||||
"platform",
|
||||
"platform_id",
|
||||
"position",
|
||||
"rack_group_id",
|
||||
"rack_id",
|
||||
"role",
|
||||
"role_id",
|
||||
"serial",
|
||||
"site",
|
||||
"site_id",
|
||||
"status",
|
||||
"tag",
|
||||
"tenant",
|
||||
"tenant_id",
|
||||
"virtual_chassis_id",
|
||||
)
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin):
|
||||
NAME = 'netbox'
|
||||
|
||||
def _fetch_information(self, url):
|
||||
|
||||
response = open_url(url, headers=self.headers, timeout=self.timeout)
|
||||
|
||||
try:
|
||||
raw_data = to_text(response.read(), errors='surrogate_or_strict')
|
||||
except UnicodeError:
|
||||
raise AnsibleError("Incorrect encoding of fetched payload from NetBox API.")
|
||||
|
||||
try:
|
||||
return json.loads(raw_data)
|
||||
except ValueError:
|
||||
raise AnsibleError("Incorrect JSON payload: %s" % raw_data)
|
||||
|
||||
def get_resource_list(self, api_url, api_token=None, specific_host=None):
|
||||
"""Retrieves resource list from netbox API.
|
||||
Returns:
|
||||
A list of all resource from netbox API.
|
||||
"""
|
||||
if not api_url:
|
||||
raise AnsibleError("Please check API URL in script configuration file.")
|
||||
api_url_headers = {}
|
||||
api_url_params = {}
|
||||
if api_token:
|
||||
api_url_headers.update({"Authorization": "Token %s" % api_token})
|
||||
if specific_host:
|
||||
api_url_params.update({"name": specific_host})
|
||||
|
||||
hosts_list = []
|
||||
|
||||
# Pagination.
|
||||
while api_url:
|
||||
self.display.v("Fetching: " + api_url)
|
||||
# Get hosts list.
|
||||
api_output = self._fetch_information(api_url)
|
||||
hosts_list += api_output["results"]
|
||||
api_url = api_output["next"]
|
||||
|
||||
# Get hosts list.
|
||||
return hosts_list
|
||||
|
||||
@property
|
||||
def group_extractors(self):
|
||||
return {
|
||||
"sites": self.extract_site,
|
||||
"tenants": self.extract_tenant,
|
||||
"racks": self.extract_rack,
|
||||
"device_roles": self.extract_device_role,
|
||||
"device_types": self.extract_device_type,
|
||||
"manufacturers": self.extract_manufacturer
|
||||
}
|
||||
|
||||
def extract_device_type(self, host):
|
||||
try:
|
||||
return self.device_types_lookup[host["device_type"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_rack(self, host):
|
||||
try:
|
||||
return self.racks_lookup[host["rack"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_site(self, host):
|
||||
try:
|
||||
return self.sites_lookup[host["site"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_tenant(self, host):
|
||||
try:
|
||||
return self.tenants_lookup[host["tenant"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_device_role(self, host):
|
||||
try:
|
||||
return self.device_roles_lookup[host["device_role"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_manufacturer(self, host):
|
||||
try:
|
||||
return self.manufacturers_lookup[host["device_type"]["manufacturer"]["id"]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_primary_ip(self, host):
|
||||
try:
|
||||
address = host["primary_ip"]["address"]
|
||||
return str(ip_interface(address).ip)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_primary_ip4(self, host):
|
||||
try:
|
||||
address = host["primary_ip4"]["address"]
|
||||
return str(ip_interface(address).ip)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_primary_ip6(self, host):
|
||||
try:
|
||||
address = host["primary_ip6"]["address"]
|
||||
return str(ip_interface(address).ip)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def refresh_sites_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/sites/?limit=0")
|
||||
sites = self.get_resource_list(api_url=url)
|
||||
self.sites_lookup = dict((site["id"], site["name"]) for site in sites)
|
||||
|
||||
def refresh_regions_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/regions/?limit=0")
|
||||
regions = self.get_resource_list(api_url=url)
|
||||
self.regions_lookup = dict((region["id"], region["name"]) for region in regions)
|
||||
|
||||
def refresh_tenants_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/tenancy/tenants/?limit=0")
|
||||
tenants = self.get_resource_list(api_url=url)
|
||||
self.tenants_lookup = dict((tenant["id"], tenant["name"]) for tenant in tenants)
|
||||
|
||||
def refresh_racks_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/racks/?limit=0")
|
||||
racks = self.get_resource_list(api_url=url)
|
||||
self.racks_lookup = dict((rack["id"], rack["name"]) for rack in racks)
|
||||
|
||||
def refresh_device_roles_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/device-roles/?limit=0")
|
||||
device_roles = self.get_resource_list(api_url=url)
|
||||
self.device_roles_lookup = dict((device_role["id"], device_role["name"]) for device_role in device_roles)
|
||||
|
||||
def refresh_device_types_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/device-types/?limit=0")
|
||||
device_types = self.get_resource_list(api_url=url)
|
||||
self.device_types_lookup = dict((device_type["id"], device_type["model"]) for device_type in device_types)
|
||||
|
||||
def refresh_manufacturers_lookup(self):
|
||||
url = urljoin(self.api_endpoint, "/api/dcim/manufacturers/?limit=0")
|
||||
manufacturers = self.get_resource_list(api_url=url)
|
||||
self.manufacturers_lookup = dict((manufacturer["id"], manufacturer["name"]) for manufacturer in manufacturers)
|
||||
|
||||
def refresh_lookups(self):
|
||||
self.refresh_sites_lookup()
|
||||
self.refresh_regions_lookup()
|
||||
self.refresh_tenants_lookup()
|
||||
self.refresh_racks_lookup()
|
||||
self.refresh_device_roles_lookup()
|
||||
self.refresh_device_types_lookup()
|
||||
self.refresh_manufacturers_lookup()
|
||||
|
||||
def validate_query_parameters(self, x):
|
||||
if not (isinstance(x, dict) and len(x) == 1):
|
||||
self.display.warning("Warning query parameters %s not a dict with a single key." % x)
|
||||
return
|
||||
|
||||
k = x.keys()[0]
|
||||
v = x.values()[0]
|
||||
|
||||
if not (k in ALLOWED_DEVICE_QUERY_PARAMETERS or k.startswith("cf_")):
|
||||
self.display.warning("Warning: %s not in %s or starting with cf (Custom field)" % (k, ALLOWED_DEVICE_QUERY_PARAMETERS))
|
||||
return
|
||||
return k, v
|
||||
|
||||
def refresh_url(self):
|
||||
query_parameters = [("limit", 0)]
|
||||
query_parameters.extend(filter(lambda x: x,
|
||||
map(self.validate_query_parameters, self.query_filters)))
|
||||
self.device_url = self.api_endpoint + "/api/dcim/devices/" + "?" + urlencode(query_parameters)
|
||||
|
||||
def fetch_hosts(self):
|
||||
return self.get_resource_list(self.device_url)
|
||||
|
||||
def extract_name(self, host):
|
||||
# An host in an Ansible inventory requires an hostname.
|
||||
# name is an unique but not required attribute for a device in NetBox
|
||||
# We default to an UUID for hostname in case the name is not set in NetBox
|
||||
return host["name"] or str(uuid.uuid4())
|
||||
|
||||
def add_host_to_groups(self, host, hostname):
|
||||
for g in self.group_by:
|
||||
group = self.group_extractors[g](host)
|
||||
|
||||
if not group:
|
||||
continue
|
||||
|
||||
group_name = "_".join([g, group])
|
||||
self.inventory.add_group(group=group_name)
|
||||
self.inventory.add_host(group=group_name, host=hostname)
|
||||
|
||||
def _fill_host_variables(self, host, hostname):
|
||||
for attribute, extractor in self.group_extractors.items():
|
||||
if not extractor(host):
|
||||
continue
|
||||
self.inventory.set_variable(hostname, attribute, extractor(host))
|
||||
|
||||
if self.extract_primary_ip(host):
|
||||
self.inventory.set_variable(hostname, "ansible_host", self.extract_primary_ip(host=host))
|
||||
|
||||
if self.extract_primary_ip4(host):
|
||||
self.inventory.set_variable(hostname, "primary_ip4", self.extract_primary_ip4(host=host))
|
||||
|
||||
if self.extract_primary_ip6(host):
|
||||
self.inventory.set_variable(hostname, "primary_ip6", self.extract_primary_ip6(host=host))
|
||||
|
||||
def main(self):
|
||||
self.refresh_lookups()
|
||||
self.refresh_url()
|
||||
hosts_list = self.fetch_hosts()
|
||||
|
||||
for host in hosts_list:
|
||||
hostname = self.extract_name(host=host)
|
||||
self.inventory.add_host(host=hostname)
|
||||
self._fill_host_variables(host=host, hostname=hostname)
|
||||
self.add_host_to_groups(host=host, hostname=hostname)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
self._read_config_data(path=path)
|
||||
|
||||
# Netbox access
|
||||
token = self.get_option("token")
|
||||
self.api_endpoint = self.get_option("api_endpoint")
|
||||
self.timeout = self.get_option("timeout")
|
||||
self.headers = {
|
||||
'Authorization': "Bearer %s" % token,
|
||||
'User-Agent': "ansible %s Python %s" % (ansible_version, python_version.split(' ')[0]),
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
|
||||
# Filter and group_by options
|
||||
self.group_by = self.get_option("group_by")
|
||||
self.query_filters = self.get_option("query_filters")
|
||||
self.main()
|
Loading…
Reference in a new issue