mirror of https://github.com/ansible/ansible.git
Migrated to netbox.netbox
parent
31d222a12f
commit
8ccb9efa1d
@ -1,357 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@fragmentedpacket) <mikhail.yohman@gmail.com>
|
||||
# Copyright: (c) 2018, David Gomez (@amb1s1) <david.gomez@networktocode.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
API_APPS_ENDPOINTS = dict(
|
||||
circuits=[],
|
||||
dcim=[
|
||||
"devices",
|
||||
"device_roles",
|
||||
"device_types",
|
||||
"devices",
|
||||
"interfaces",
|
||||
"platforms",
|
||||
"racks",
|
||||
"regions",
|
||||
"sites",
|
||||
],
|
||||
extras=[],
|
||||
ipam=["ip_addresses", "prefixes", "roles", "vlans", "vlan_groups", "vrfs"],
|
||||
secrets=[],
|
||||
tenancy=["tenants", "tenant_groups"],
|
||||
virtualization=["clusters"],
|
||||
)
|
||||
|
||||
QUERY_TYPES = dict(
|
||||
cluster="name",
|
||||
devices="name",
|
||||
device_role="slug",
|
||||
device_type="slug",
|
||||
manufacturer="slug",
|
||||
nat_inside="address",
|
||||
nat_outside="address",
|
||||
platform="slug",
|
||||
primary_ip="address",
|
||||
primary_ip4="address",
|
||||
primary_ip6="address",
|
||||
rack="slug",
|
||||
region="slug",
|
||||
role="slug",
|
||||
site="slug",
|
||||
tenant="name",
|
||||
tenant_group="slug",
|
||||
time_zone="timezone",
|
||||
vlan="name",
|
||||
vlan_group="slug",
|
||||
vrf="name",
|
||||
)
|
||||
|
||||
CONVERT_TO_ID = dict(
|
||||
cluster="clusters",
|
||||
device="devices",
|
||||
device_role="device_roles",
|
||||
device_type="device_types",
|
||||
interface="interfaces",
|
||||
lag="interfaces",
|
||||
nat_inside="ip_addresses",
|
||||
nat_outside="ip_addresses",
|
||||
platform="platforms",
|
||||
primary_ip="ip_addresses",
|
||||
primary_ip4="ip_addresses",
|
||||
primary_ip6="ip_addresses",
|
||||
rack="racks",
|
||||
region="regions",
|
||||
role="roles",
|
||||
site="sites",
|
||||
tagged_vlans="vlans",
|
||||
tenant="tenants",
|
||||
tenant_group="tenant_groups",
|
||||
untagged_vlan="vlans",
|
||||
vlan="vlans",
|
||||
vlan_group="vlan_groups",
|
||||
vrf="vrfs",
|
||||
)
|
||||
|
||||
FACE_ID = dict(front=0, rear=1)
|
||||
|
||||
NO_DEFAULT_ID = set(
|
||||
[
|
||||
"device",
|
||||
"lag",
|
||||
"primary_ip",
|
||||
"primary_ip4",
|
||||
"primary_ip6",
|
||||
"role",
|
||||
"vlan",
|
||||
"vrf",
|
||||
"nat_inside",
|
||||
"nat_outside",
|
||||
"region",
|
||||
"untagged_vlan",
|
||||
"tagged_vlans",
|
||||
"tenant",
|
||||
]
|
||||
)
|
||||
|
||||
DEVICE_STATUS = dict(offline=0, active=1, planned=2, staged=3, failed=4, inventory=5)
|
||||
|
||||
IP_ADDRESS_STATUS = dict(active=1, reserved=2, deprecated=3, dhcp=5)
|
||||
|
||||
IP_ADDRESS_ROLE = dict(
|
||||
loopback=10, secondary=20, anycast=30, vip=40, vrrp=41, hsrp=42, glbp=43, carp=44
|
||||
)
|
||||
|
||||
PREFIX_STATUS = dict(container=0, active=1, reserved=2, deprecated=3)
|
||||
|
||||
VLAN_STATUS = dict(active=1, reserved=2, deprecated=3)
|
||||
|
||||
SITE_STATUS = dict(active=1, planned=2, retired=4)
|
||||
|
||||
INTF_FORM_FACTOR = {
|
||||
"virtual": 0,
|
||||
"link aggregation group (lag)": 200,
|
||||
"100base-tx (10/100me)": 800,
|
||||
"1000base-t (1ge)": 1000,
|
||||
"10gbase-t (10ge)": 1150,
|
||||
"10gbase-cx4 (10ge)": 1170,
|
||||
"gbic (1ge)": 1050,
|
||||
"sfp (1ge)": 1100,
|
||||
"sfp+ (10ge)": 1200,
|
||||
"xfp (10ge)": 1300,
|
||||
"xenpak (10ge)": 1310,
|
||||
"x2 (10ge)": 1320,
|
||||
"sfp28 (25ge)": 1350,
|
||||
"qsfp+ (40ge)": 1400,
|
||||
"cfp (100ge)": 1500,
|
||||
"cfp2 (100ge)": 1510,
|
||||
"cfp2 (200ge)": 1650,
|
||||
"cfp4 (100ge)": 1520,
|
||||
"cisco cpak (100ge)": 1550,
|
||||
"qsfp28 (100ge)": 1600,
|
||||
"qsfp56 (200ge)": 1700,
|
||||
"qsfp-dd (400ge)": 1750,
|
||||
"ieee 802.11a": 2600,
|
||||
"ieee 802.11b/g": 2610,
|
||||
"ieee 802.11n": 2620,
|
||||
"ieee 802.11ac": 2630,
|
||||
"ieee 802.11ad": 2640,
|
||||
"gsm": 2810,
|
||||
"cdma": 2820,
|
||||
"lte": 2830,
|
||||
"oc-3/stm-1": 6100,
|
||||
"oc-12/stm-4": 6200,
|
||||
"oc-48/stm-16": 6300,
|
||||
"oc-192/stm-64": 6400,
|
||||
"oc-768/stm-256": 6500,
|
||||
"oc-1920/stm-640": 6600,
|
||||
"oc-3840/stm-1234": 6700,
|
||||
"sfp (1gfc)": 3010,
|
||||
"sfp (2gfc)": 3020,
|
||||
"sfp (4gfc)": 3040,
|
||||
"sfp+ (8gfc)": 3080,
|
||||
"sfp+ (16gfc)": 3160,
|
||||
"sfp28 (32gfc)": 3320,
|
||||
"qsfp28 (128gfc)": 3400,
|
||||
"t1 (1.544 mbps)": 4000,
|
||||
"e1 (2.048 mbps)": 4010,
|
||||
"t3 (45 mbps)": 4040,
|
||||
"e3 (34 mbps)": 4050,
|
||||
"cisco stackwise": 5000,
|
||||
"cisco stackwise plus": 5050,
|
||||
"cisco flexstack": 5100,
|
||||
"cisco flexstack plus": 5150,
|
||||
"juniper vcp": 5200,
|
||||
"extreme summitstack": 5300,
|
||||
"extreme summitstack-128": 5310,
|
||||
"extreme summitstack-256": 5320,
|
||||
"extreme summitstack-512": 5330,
|
||||
"other": 32767,
|
||||
}
|
||||
|
||||
INTF_MODE = {"access": 100, "tagged": 200, "tagged all": 300}
|
||||
|
||||
ALLOWED_QUERY_PARAMS = {
|
||||
"interface": set(["name", "device"]),
|
||||
"lag": set(["name"]),
|
||||
"nat_inside": set(["vrf", "address"]),
|
||||
"vlan": set(["name", "site", "vlan_group", "tenant"]),
|
||||
"untagged_vlan": set(["name", "site", "vlan_group", "tenant"]),
|
||||
"tagged_vlans": set(["name", "site", "vlan_group", "tenant"]),
|
||||
}
|
||||
|
||||
QUERY_PARAMS_IDS = set(["vrf", "site", "vlan_group", "tenant"])
|
||||
|
||||
|
||||
def _build_diff(before=None, after=None):
|
||||
return {"before": before, "after": after}
|
||||
|
||||
|
||||
def create_netbox_object(nb_endpoint, data, check_mode):
|
||||
"""Create a Netbox object.
|
||||
:returns tuple(serialized_nb_obj, diff): tuple of the serialized created
|
||||
Netbox object and the Ansible diff.
|
||||
"""
|
||||
if check_mode:
|
||||
serialized_nb_obj = data
|
||||
else:
|
||||
nb_obj = nb_endpoint.create(data)
|
||||
try:
|
||||
serialized_nb_obj = nb_obj.serialize()
|
||||
except AttributeError:
|
||||
serialized_nb_obj = nb_obj
|
||||
|
||||
diff = _build_diff(before={"state": "absent"}, after={"state": "present"})
|
||||
return serialized_nb_obj, diff
|
||||
|
||||
|
||||
def delete_netbox_object(nb_obj, check_mode):
|
||||
"""Delete a Netbox object.
|
||||
:returns tuple(serialized_nb_obj, diff): tuple of the serialized deleted
|
||||
Netbox object and the Ansible diff.
|
||||
"""
|
||||
if not check_mode:
|
||||
nb_obj.delete()
|
||||
|
||||
diff = _build_diff(before={"state": "present"}, after={"state": "absent"})
|
||||
return nb_obj.serialize(), diff
|
||||
|
||||
|
||||
def update_netbox_object(nb_obj, data, check_mode):
|
||||
"""Update a Netbox object.
|
||||
:returns tuple(serialized_nb_obj, diff): tuple of the serialized updated
|
||||
Netbox object and the Ansible diff.
|
||||
"""
|
||||
serialized_nb_obj = nb_obj.serialize()
|
||||
updated_obj = serialized_nb_obj.copy()
|
||||
updated_obj.update(data)
|
||||
if serialized_nb_obj == updated_obj:
|
||||
return serialized_nb_obj, None
|
||||
else:
|
||||
data_before, data_after = {}, {}
|
||||
for key in data:
|
||||
if serialized_nb_obj[key] != updated_obj[key]:
|
||||
data_before[key] = serialized_nb_obj[key]
|
||||
data_after[key] = updated_obj[key]
|
||||
|
||||
if not check_mode:
|
||||
nb_obj.update(data)
|
||||
updated_obj = nb_obj.serialize()
|
||||
|
||||
diff = _build_diff(before=data_before, after=data_after)
|
||||
return updated_obj, diff
|
||||
|
||||
|
||||
def _get_query_param_id(nb, match, child):
|
||||
endpoint = CONVERT_TO_ID[match]
|
||||
app = find_app(endpoint)
|
||||
nb_app = getattr(nb, app)
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
result = nb_endpoint.get(**{QUERY_TYPES.get(match): child[match]})
|
||||
if result:
|
||||
return result.id
|
||||
else:
|
||||
return child
|
||||
|
||||
|
||||
def find_app(endpoint):
|
||||
for k, v in API_APPS_ENDPOINTS.items():
|
||||
if endpoint in v:
|
||||
nb_app = k
|
||||
return nb_app
|
||||
|
||||
|
||||
def build_query_params(nb, parent, module_data, child):
|
||||
query_dict = dict()
|
||||
query_params = ALLOWED_QUERY_PARAMS.get(parent)
|
||||
matches = query_params.intersection(set(child.keys()))
|
||||
for match in matches:
|
||||
if match in QUERY_PARAMS_IDS:
|
||||
value = _get_query_param_id(nb, match, child)
|
||||
query_dict.update({match + "_id": value})
|
||||
else:
|
||||
value = child.get(match)
|
||||
query_dict.update({match: value})
|
||||
|
||||
if parent == "lag":
|
||||
query_dict.update({"form_factor": 200})
|
||||
if isinstance(module_data["device"], int):
|
||||
query_dict.update({"device_id": module_data["device"]})
|
||||
else:
|
||||
query_dict.update({"device": module_data["device"]})
|
||||
|
||||
return query_dict
|
||||
|
||||
|
||||
def find_ids(nb, data):
|
||||
for k, v in data.items():
|
||||
if k in CONVERT_TO_ID:
|
||||
endpoint = CONVERT_TO_ID[k]
|
||||
search = v
|
||||
app = find_app(endpoint)
|
||||
nb_app = getattr(nb, app)
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
|
||||
if isinstance(v, dict):
|
||||
query_params = build_query_params(nb, k, data, v)
|
||||
query_id = nb_endpoint.get(**query_params)
|
||||
|
||||
elif isinstance(v, list):
|
||||
id_list = list()
|
||||
for index in v:
|
||||
norm_data = normalize_data(index)
|
||||
temp_dict = build_query_params(nb, k, data, norm_data)
|
||||
query_id = nb_endpoint.get(**temp_dict)
|
||||
if query_id:
|
||||
id_list.append(query_id.id)
|
||||
else:
|
||||
return ValueError("%s not found" % (index))
|
||||
|
||||
else:
|
||||
try:
|
||||
query_id = nb_endpoint.get(**{QUERY_TYPES.get(k, "q"): search})
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"Multiple results found while searching for key: %s" % (k)
|
||||
)
|
||||
|
||||
if isinstance(v, list):
|
||||
data[k] = id_list
|
||||
elif query_id:
|
||||
data[k] = query_id.id
|
||||
elif k in NO_DEFAULT_ID:
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Could not resolve id of %s: %s" % (k, v))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def normalize_data(data):
|
||||
for k, v in data.items():
|
||||
if isinstance(v, dict):
|
||||
for subk, subv in v.items():
|
||||
sub_data_type = QUERY_TYPES.get(subk, "q")
|
||||
if sub_data_type == "slug":
|
||||
if "-" in subv:
|
||||
data[k][subk] = subv.replace(" ", "").lower()
|
||||
elif " " in subv:
|
||||
data[k][subk] = subv.replace(" ", "-").lower()
|
||||
else:
|
||||
data[k][subk] = subv.lower()
|
||||
else:
|
||||
data_type = QUERY_TYPES.get(k, "q")
|
||||
if data_type == "slug":
|
||||
if "-" in v:
|
||||
data[k] = v.replace(" ", "").lower()
|
||||
elif " " in v:
|
||||
data[k] = v.replace(" ", "-").lower()
|
||||
else:
|
||||
data[k] = v.lower()
|
||||
elif data_type == "timezone":
|
||||
if " " in v:
|
||||
data[k] = v.replace(" ", "_")
|
||||
|
||||
return data
|
@ -1,312 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>
|
||||
# Copyright: (c) 2018, David Gomez (@amb1s1) <david.gomez@networktocode.com>
|
||||
# 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
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: netbox_device
|
||||
short_description: Create, update or delete devices within Netbox
|
||||
description:
|
||||
- Creates, updates or removes devices from Netbox
|
||||
notes:
|
||||
- Tags should be defined as a YAML list
|
||||
- This should be ran with connection C(local) and hosts C(localhost)
|
||||
author:
|
||||
- Mikhail Yohman (@FragmentedPacket)
|
||||
- David Gomez (@amb1s1)
|
||||
requirements:
|
||||
- pynetbox
|
||||
version_added: '2.8'
|
||||
options:
|
||||
netbox_url:
|
||||
description:
|
||||
- URL of the Netbox instance resolvable by Ansible control host
|
||||
required: true
|
||||
netbox_token:
|
||||
description:
|
||||
- The token created within Netbox to authorize API access
|
||||
required: true
|
||||
data:
|
||||
description:
|
||||
- Defines the device configuration
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the device
|
||||
required: true
|
||||
device_type:
|
||||
description:
|
||||
- Required if I(state=present) and the device does not exist yet
|
||||
device_role:
|
||||
description:
|
||||
- Required if I(state=present) and the device does not exist yet
|
||||
tenant:
|
||||
description:
|
||||
- The tenant that the device will be assigned to
|
||||
platform:
|
||||
description:
|
||||
- The platform of the device
|
||||
serial:
|
||||
description:
|
||||
- Serial number of the device
|
||||
asset_tag:
|
||||
description:
|
||||
- Asset tag that is associated to the device
|
||||
site:
|
||||
description:
|
||||
- Required if I(state=present) and the device does not exist yet
|
||||
rack:
|
||||
description:
|
||||
- The name of the rack to assign the device to
|
||||
position:
|
||||
description:
|
||||
- The position of the device in the rack defined above
|
||||
face:
|
||||
description:
|
||||
- Required if I(rack) is defined
|
||||
status:
|
||||
description:
|
||||
- The status of the device
|
||||
choices:
|
||||
- Active
|
||||
- Offline
|
||||
- Planned
|
||||
- Staged
|
||||
- Failed
|
||||
- Inventory
|
||||
cluster:
|
||||
description:
|
||||
- Cluster that the device will be assigned to
|
||||
comments:
|
||||
description:
|
||||
- Comments that may include additional information in regards to the device
|
||||
tags:
|
||||
description:
|
||||
- Any tags that the device may need to be associated with
|
||||
custom_fields:
|
||||
description:
|
||||
- must exist in Netbox
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates.
|
||||
default: 'yes'
|
||||
type: bool
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: "Test Netbox modules"
|
||||
connection: local
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: Create device within Netbox with only required information
|
||||
netbox_device:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test Device
|
||||
device_type: C9410R
|
||||
device_role: Core Switch
|
||||
site: Main
|
||||
state: present
|
||||
|
||||
- name: Delete device within netbox
|
||||
netbox_device:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test Device
|
||||
state: absent
|
||||
|
||||
- name: Create device with tags
|
||||
netbox_device:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Another Test Device
|
||||
device_type: C9410R
|
||||
device_role: Core Switch
|
||||
site: Main
|
||||
tags:
|
||||
- Schnozzberry
|
||||
state: present
|
||||
|
||||
- name: Update the rack and position of an existing device
|
||||
netbox_device:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test Device
|
||||
rack: Test Rack
|
||||
position: 10
|
||||
face: Front
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
device:
|
||||
description: Serialized object as created or already existent within Netbox
|
||||
returned: success (when I(state=present))
|
||||
type: dict
|
||||
msg:
|
||||
description: Message indicating failure or info about what has been achieved
|
||||
returned: always
|
||||
type: str
|
||||
'''
|
||||
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
find_ids,
|
||||
normalize_data,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
DEVICE_STATUS,
|
||||
FACE_ID
|
||||
)
|
||||
|
||||
PYNETBOX_IMP_ERR = None
|
||||
try:
|
||||
import pynetbox
|
||||
HAS_PYNETBOX = True
|
||||
except ImportError:
|
||||
PYNETBOX_IMP_ERR = traceback.format_exc()
|
||||
HAS_PYNETBOX = False
|
||||
|
||||
|
||||
def main():
|
||||
'''
|
||||
Main entry point for module execution
|
||||
'''
|
||||
argument_spec = dict(
|
||||
netbox_url=dict(type="str", required=True),
|
||||
netbox_token=dict(type="str", required=True, no_log=True),
|
||||
data=dict(type="dict", required=True),
|
||||
state=dict(required=False, default='present', choices=['present', 'absent']),
|
||||
validate_certs=dict(type="bool", default=True)
|
||||
)
|
||||
|
||||
global module
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# Fail module if pynetbox is not installed
|
||||
if not HAS_PYNETBOX:
|
||||
module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR)
|
||||
|
||||
# Fail if device name is not given
|
||||
if not module.params["data"].get("name"):
|
||||
module.fail_json(msg="missing device name")
|
||||
|
||||
# Assign variables to be used with module
|
||||
app = 'dcim'
|
||||
endpoint = 'devices'
|
||||
url = module.params["netbox_url"]
|
||||
token = module.params["netbox_token"]
|
||||
data = module.params["data"]
|
||||
state = module.params["state"]
|
||||
validate_certs = module.params["validate_certs"]
|
||||
|
||||
# Attempt to create Netbox API object
|
||||
try:
|
||||
nb = pynetbox.api(url, token=token, ssl_verify=validate_certs)
|
||||
except Exception:
|
||||
module.fail_json(msg="Failed to establish connection to Netbox API")
|
||||
try:
|
||||
nb_app = getattr(nb, app)
|
||||
except AttributeError:
|
||||
module.fail_json(msg="Incorrect application specified: %s" % (app))
|
||||
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
norm_data = normalize_data(data)
|
||||
try:
|
||||
if 'present' in state:
|
||||
result = ensure_device_present(nb, nb_endpoint, norm_data)
|
||||
else:
|
||||
result = ensure_device_absent(nb_endpoint, norm_data)
|
||||
return module.exit_json(**result)
|
||||
except pynetbox.RequestError as e:
|
||||
return module.fail_json(msg=json.loads(e.error))
|
||||
except ValueError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def _find_ids(nb, data):
|
||||
if data.get("status"):
|
||||
data["status"] = DEVICE_STATUS.get(data["status"].lower())
|
||||
if data.get("face"):
|
||||
data["face"] = FACE_ID.get(data["face"].lower())
|
||||
return find_ids(nb, data)
|
||||
|
||||
|
||||
def ensure_device_present(nb, nb_endpoint, normalized_data):
|
||||
'''
|
||||
:returns dict(device, msg, changed, diff): dictionary resulting of the request,
|
||||
where `device` is the serialized device fetched or newly created in
|
||||
Netbox
|
||||
'''
|
||||
data = _find_ids(nb, normalized_data)
|
||||
nb_device = nb_endpoint.get(name=data["name"])
|
||||
result = {}
|
||||
if not nb_device:
|
||||
device, diff = create_netbox_object(nb_endpoint, data, module.check_mode)
|
||||
msg = "Device %s created" % (data["name"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
device, diff = update_netbox_object(nb_device, data, module.check_mode)
|
||||
if device is False:
|
||||
module.fail_json(
|
||||
msg="Request failed, couldn't update device: %s" % data["name"]
|
||||
)
|
||||
if diff:
|
||||
msg = "Device %s updated" % (data["name"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Device %s already exists" % (data["name"])
|
||||
changed = False
|
||||
result.update({"device": device, "changed": changed, "msg": msg})
|
||||
return result
|
||||
|
||||
|
||||
def ensure_device_absent(nb_endpoint, data):
|
||||
'''
|
||||
:returns dict(msg, changed, diff)
|
||||
'''
|
||||
nb_device = nb_endpoint.get(name=data["name"])
|
||||
result = {}
|
||||
if nb_device:
|
||||
dummy, diff = delete_netbox_object(nb_device, module.check_mode)
|
||||
msg = 'Device %s deleted' % (data["name"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = 'Device %s already absent' % (data["name"])
|
||||
changed = False
|
||||
|
||||
result.update({"changed": changed, "msg": msg})
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,351 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>
|
||||
# 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
|
||||
|
||||
ANSIBLE_METADATA = {"metadata_version": "1.1",
|
||||
"status": ["preview"],
|
||||
"supported_by": "community"}
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: netbox_interface
|
||||
short_description: Creates or removes interfaces from Netbox
|
||||
description:
|
||||
- Creates or removes interfaces from Netbox
|
||||
notes:
|
||||
- Tags should be defined as a YAML list
|
||||
- This should be ran with connection C(local) and hosts C(localhost)
|
||||
author:
|
||||
- Mikhail Yohman (@FragmentedPacket)
|
||||
requirements:
|
||||
- pynetbox
|
||||
version_added: "2.8"
|
||||
options:
|
||||
netbox_url:
|
||||
description:
|
||||
- URL of the Netbox instance resolvable by Ansible control host
|
||||
required: true
|
||||
type: str
|
||||
netbox_token:
|
||||
description:
|
||||
- The token created within Netbox to authorize API access
|
||||
required: true
|
||||
type: str
|
||||
data:
|
||||
description:
|
||||
- Defines the prefix configuration
|
||||
suboptions:
|
||||
device:
|
||||
description:
|
||||
- Name of the device the interface will be associated with (case-sensitive)
|
||||
required: true
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the interface to be created
|
||||
required: true
|
||||
type: str
|
||||
form_factor:
|
||||
description:
|
||||
- |
|
||||
Form factor of the interface:
|
||||
ex. 1000Base-T (1GE), Virtual, 10GBASE-T (10GE)
|
||||
This has to be specified exactly as what is found within UI
|
||||
type: str
|
||||
enabled:
|
||||
description:
|
||||
- Sets whether interface shows enabled or disabled
|
||||
type: bool
|
||||
lag:
|
||||
description:
|
||||
- Parent LAG interface will be a member of
|
||||
type: dict
|
||||
mtu:
|
||||
description:
|
||||
- The MTU of the interface
|
||||
type: str
|
||||
mac_address:
|
||||
description:
|
||||
- The MAC address of the interface
|
||||
type: str
|
||||
mgmt_only:
|
||||
description:
|
||||
- This interface is used only for out-of-band management
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
- The description of the prefix
|
||||
type: str
|
||||
mode:
|
||||
description:
|
||||
- The mode of the interface
|
||||
choices:
|
||||
- Access
|
||||
- Tagged
|
||||
- Tagged All
|
||||
type: str
|
||||
untagged_vlan:
|
||||
description:
|
||||
- The untagged VLAN to be assigned to interface
|
||||
type: dict
|
||||
tagged_vlans:
|
||||
description:
|
||||
- A list of tagged VLANS to be assigned to interface. Mode must be set to either C(Tagged) or C(Tagged All)
|
||||
type: list
|
||||
tags:
|
||||
description:
|
||||
- Any tags that the prefix may need to be associated with
|
||||
type: list
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
- |
|
||||
If C(no), SSL certificates will not be validated.
|
||||
This should only be used on personally controlled sites using self-signed certificates.
|
||||
default: "yes"
|
||||
type: bool
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: "Test Netbox interface module"
|
||||
connection: local
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Create interface within Netbox with only required information
|
||||
netbox_interface:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
device: test100
|
||||
name: GigabitEthernet1
|
||||
state: present
|
||||
- name: Delete interface within netbox
|
||||
netbox_interface:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
device: test100
|
||||
name: GigabitEthernet1
|
||||
state: absent
|
||||
- name: Create LAG with several specified options
|
||||
netbox_interface:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
device: test100
|
||||
name: port-channel1
|
||||
form_factor: Link Aggregation Group (LAG)
|
||||
mtu: 1600
|
||||
mgmt_only: false
|
||||
mode: Access
|
||||
state: present
|
||||
- name: Create interface and assign it to parent LAG
|
||||
netbox_interface:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
device: test100
|
||||
name: GigabitEthernet1
|
||||
enabled: false
|
||||
form_factor: 1000Base-t (1GE)
|
||||
lag:
|
||||
name: port-channel1
|
||||
mtu: 1600
|
||||
mgmt_only: false
|
||||
mode: Access
|
||||
state: present
|
||||
- name: Create interface as a trunk port
|
||||
netbox_interface:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
device: test100
|
||||
name: GigabitEthernet25
|
||||
enabled: false
|
||||
form_factor: 1000Base-t (1GE)
|
||||
untagged_vlan:
|
||||
name: Wireless
|
||||
site: Test Site
|
||||
tagged_vlans:
|
||||
- name: Data
|
||||
site: Test Site
|
||||
- name: VoIP
|
||||
site: Test Site
|
||||
mtu: 1600
|
||||
mgmt_only: true
|
||||
mode: Tagged
|
||||
state: present
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
interface:
|
||||
description: Serialized object as created or already existent within Netbox
|
||||
returned: on creation
|
||||
type: dict
|
||||
msg:
|
||||
description: Message indicating failure or info about what has been achieved
|
||||
returned: always
|
||||
type: str
|
||||
"""
|
||||
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
find_ids,
|
||||
normalize_data,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
INTF_FORM_FACTOR,
|
||||
INTF_MODE,
|
||||
)
|
||||
from ansible.module_utils.compat import ipaddress
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
PYNETBOX_IMP_ERR = None
|
||||
try:
|
||||
import pynetbox
|
||||
HAS_PYNETBOX = True
|
||||
except ImportError:
|
||||
PYNETBOX_IMP_ERR = traceback.format_exc()
|
||||
HAS_PYNETBOX = False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
netbox_url=dict(type="str", required=True),
|
||||
netbox_token=dict(type="str", required=True, no_log=True),
|
||||
data=dict(type="dict", required=True),
|
||||
state=dict(required=False, default="present", choices=["present", "absent"]),
|
||||
validate_certs=dict(type="bool", default=True)
|
||||
)
|
||||
|
||||
global module
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# Fail module if pynetbox is not installed
|
||||
if not HAS_PYNETBOX:
|
||||
module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR)
|
||||
# Assign variables to be used with module
|
||||
app = "dcim"
|
||||
endpoint = "interfaces"
|
||||
url = module.params["netbox_url"]
|
||||
token = module.params["netbox_token"]
|
||||
data = module.params["data"]
|
||||
state = module.params["state"]
|
||||
validate_certs = module.params["validate_certs"]
|
||||
# Attempt to create Netbox API object
|
||||
try:
|
||||
nb = pynetbox.api(url, token=token, ssl_verify=validate_certs)
|
||||
except Exception:
|
||||
module.fail_json(msg="Failed to establish connection to Netbox API")
|
||||
try:
|
||||
nb_app = getattr(nb, app)
|
||||
except AttributeError:
|
||||
module.fail_json(msg="Incorrect application specified: %s" % (app))
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
norm_data = normalize_data(data)
|
||||
try:
|
||||
norm_data = _check_and_adapt_data(nb, norm_data)
|
||||
|
||||
if "present" in state:
|
||||
return module.exit_json(
|
||||
**ensure_interface_present(nb, nb_endpoint, norm_data)
|
||||
)
|
||||
else:
|
||||
return module.exit_json(
|
||||
**ensure_interface_absent(nb, nb_endpoint, norm_data)
|
||||
)
|
||||
except pynetbox.RequestError as e:
|
||||
return module.fail_json(msg=json.loads(e.error))
|
||||
except ValueError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
except AttributeError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def _check_and_adapt_data(nb, data):
|
||||
data = find_ids(nb, data)
|
||||
|
||||
if data.get("form_factor"):
|
||||
data["form_factor"] = INTF_FORM_FACTOR.get(data["form_factor"].lower())
|
||||
if data.get("mode"):
|
||||
data["mode"] = INTF_MODE.get(data["mode"].lower())
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def ensure_interface_present(nb, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(interface, msg, changed): dictionary resulting of the request,
|
||||
where 'interface' is the serialized interface fetched or newly created in Netbox
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
nb_intf = nb_endpoint.get(name=data["name"], device_id=data["device"])
|
||||
result = dict()
|
||||
|
||||
if not nb_intf:
|
||||
intf, diff = create_netbox_object(nb_endpoint, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "Interface %s created" % (data["name"])
|
||||
else:
|
||||
intf, diff = update_netbox_object(nb_intf, data, module.check_mode)
|
||||
if intf is False:
|
||||
module.fail_json(
|
||||
msg="Request failed, couldn't update device: %s" % (data["name"])
|
||||
)
|
||||
if diff:
|
||||
msg = "Interface %s updated" % (data["name"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Interface %s already exists" % (data["name"])
|
||||
changed = False
|
||||
result.update({"interface": intf, "msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
def ensure_interface_absent(nb, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(msg, changed, diff)
|
||||
"""
|
||||
nb_intf = nb_endpoint.get(name=data["name"], device_id=data["device"])
|
||||
result = dict()
|
||||
if nb_intf:
|
||||
dummy, diff = delete_netbox_object(nb_intf, module.check_mode)
|
||||
changed = True
|
||||
msg = "Interface %s deleted" % (data["name"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Interface %s already absent" % (data["name"])
|
||||
changed = False
|
||||
|
||||
result.update({"msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,517 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>
|
||||
# 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
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: netbox_ip_address
|
||||
short_description: Creates or removes IP addresses from Netbox
|
||||
description:
|
||||
- Creates or removes IP addresses from Netbox
|
||||
notes:
|
||||
- Tags should be defined as a YAML list
|
||||
- This should be ran with connection C(local) and hosts C(localhost)
|
||||
author:
|
||||
- Mikhail Yohman (@FragmentedPacket)
|
||||
- Anthony Ruhier (@Anthony25)
|
||||
requirements:
|
||||
- pynetbox
|
||||
version_added: '2.8'
|
||||
options:
|
||||
netbox_url:
|
||||
description:
|
||||
- URL of the Netbox instance resolvable by Ansible control host
|
||||
required: true
|
||||
netbox_token:
|
||||
description:
|
||||
- The token created within Netbox to authorize API access
|
||||
required: true
|
||||
data:
|
||||
description:
|
||||
- Defines the IP address configuration
|
||||
suboptions:
|
||||
family:
|
||||
description:
|
||||
- Specifies with address family the IP address belongs to
|
||||
choices:
|
||||
- 4
|
||||
- 6
|
||||
address:
|
||||
description:
|
||||
- Required if state is C(present)
|
||||
prefix:
|
||||
description:
|
||||
- |
|
||||
With state C(present), if an interface is given, it will ensure
|
||||
that an IP inside this prefix (and vrf, if given) is attached
|
||||
to this interface. Otherwise, it will get the next available IP
|
||||
of this prefix and attach it.
|
||||
With state C(new), it will force to get the next available IP in
|
||||
this prefix. If an interface is given, it will also force to attach
|
||||
it.
|
||||
Required if state is C(present) or C(new) when no address is given.
|
||||
Unused if an address is specified.
|
||||
vrf:
|
||||
description:
|
||||
- VRF that IP address is associated with
|
||||
tenant:
|
||||
description:
|
||||
- The tenant that the device will be assigned to
|
||||
status:
|
||||
description:
|
||||
- The status of the IP address
|
||||
choices:
|
||||
- Active
|
||||
- Reserved
|
||||
- Deprecated
|
||||
- DHCP
|
||||
role:
|
||||
description:
|
||||
- The role of the IP address
|
||||
choices:
|
||||
- Loopback
|
||||
- Secondary
|
||||
- Anycast
|
||||
- VIP
|
||||
- VRRP
|
||||
- HSRP
|
||||
- GLBP
|
||||
- CARP
|
||||
interface:
|
||||
description:
|
||||
- |
|
||||
The name and device of the interface that the IP address should be assigned to
|
||||
Required if state is C(present) and a prefix specified.
|
||||
description:
|
||||
description:
|
||||
- The description of the interface
|
||||
nat_inside:
|
||||
description:
|
||||
- The inside IP address this IP is assigned to
|
||||
tags:
|
||||
description:
|
||||
- Any tags that the IP address may need to be associated with
|
||||
custom_fields:
|
||||
description:
|
||||
- must exist in Netbox
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- |
|
||||
Use C(present), C(new) or C(absent) for adding, force adding or removing.
|
||||
C(present) will check if the IP is already created, and return it if
|
||||
true. C(new) will force to create it anyway (useful for anycasts, for
|
||||
example).
|
||||
choices: [ absent, new, present ]
|
||||
default: present
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates.
|
||||
default: 'yes'
|
||||
type: bool
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: "Test Netbox IP address module"
|
||||
connection: local
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: Create IP address within Netbox with only required information
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
address: 192.168.1.10
|
||||
state: present
|
||||
- name: Force to create (even if it already exists) the IP
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
address: 192.168.1.10
|
||||
state: new
|
||||
- name: Get a new available IP inside 192.168.1.0/24
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 192.168.1.0/24
|
||||
state: new
|
||||
- name: Delete IP address within netbox
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
address: 192.168.1.10
|
||||
state: absent
|
||||
- name: Create IP address with several specified options
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
family: 4
|
||||
address: 192.168.1.20
|
||||
vrf: Test
|
||||
tenant: Test Tenant
|
||||
status: Reserved
|
||||
role: Loopback
|
||||
description: Test description
|
||||
tags:
|
||||
- Schnozzberry
|
||||
state: present
|
||||
- name: Create IP address and assign a nat_inside IP
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
family: 4
|
||||
address: 192.168.1.30
|
||||
vrf: Test
|
||||
nat_inside:
|
||||
address: 192.168.1.20
|
||||
vrf: Test
|
||||
interface:
|
||||
name: GigabitEthernet1
|
||||
device: test100
|
||||
- name: Ensure that an IP inside 192.168.1.0/24 is attached to GigabitEthernet1
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 192.168.1.0/24
|
||||
vrf: Test
|
||||
interface:
|
||||
name: GigabitEthernet1
|
||||
device: test100
|
||||
state: present
|
||||
- name: Attach a new available IP of 192.168.1.0/24 to GigabitEthernet1
|
||||
netbox_ip_address:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 192.168.1.0/24
|
||||
vrf: Test
|
||||
interface:
|
||||
name: GigabitEthernet1
|
||||
device: test100
|
||||
state: new
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
ip_address:
|
||||
description: Serialized object as created or already existent within Netbox
|
||||
returned: on creation
|
||||
type: dict
|
||||
msg:
|
||||
description: Message indicating failure or info about what has been achieved
|
||||
returned: always
|
||||
type: str
|
||||
'''
|
||||
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
find_ids,
|
||||
normalize_data,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
IP_ADDRESS_ROLE,
|
||||
IP_ADDRESS_STATUS
|
||||
)
|
||||
from ansible.module_utils.compat import ipaddress
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
PYNETBOX_IMP_ERR = None
|
||||
try:
|
||||
import pynetbox
|
||||
HAS_PYNETBOX = True
|
||||
except ImportError:
|
||||
PYNETBOX_IMP_ERR = traceback.format_exc()
|
||||
HAS_PYNETBOX = False
|
||||
|
||||
|
||||
def main():
|
||||
'''
|
||||
Main entry point for module execution
|
||||
'''
|
||||
argument_spec = dict(
|
||||
netbox_url=dict(type="str", required=True),
|
||||
netbox_token=dict(type="str", required=True, no_log=True),
|
||||
data=dict(type="dict", required=True),
|
||||
state=dict(required=False, default='present', choices=['present', 'absent', 'new']),
|
||||
validate_certs=dict(type="bool", default=True)
|
||||
)
|
||||
|
||||
global module
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# Fail module if pynetbox is not installed
|
||||
if not HAS_PYNETBOX:
|
||||
module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR)
|
||||
|
||||
# Assign variables to be used with module
|
||||
changed = False
|
||||
app = 'ipam'
|
||||
endpoint = 'ip_addresses'
|
||||
url = module.params["netbox_url"]
|
||||
token = module.params["netbox_token"]
|
||||
data = module.params["data"]
|
||||
state = module.params["state"]
|
||||
validate_certs = module.params["validate_certs"]
|
||||
|
||||
# Attempt to create Netbox API object
|
||||
try:
|
||||
nb = pynetbox.api(url, token=token, ssl_verify=validate_certs)
|
||||
except Exception:
|
||||
module.fail_json(msg="Failed to establish connection to Netbox API")
|
||||
try:
|
||||
nb_app = getattr(nb, app)
|
||||
except AttributeError:
|
||||
module.fail_json(msg="Incorrect application specified: %s" % (app))
|
||||
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
norm_data = normalize_data(data)
|
||||
try:
|
||||
norm_data = _check_and_adapt_data(nb, norm_data)
|
||||
if state in ("new", "present"):
|
||||
return _handle_state_new_present(
|
||||
module, state, nb_app, nb_endpoint, norm_data
|
||||
)
|
||||
elif state == "absent":
|
||||
return module.exit_json(
|
||||
**ensure_ip_address_absent(nb_endpoint, norm_data)
|
||||
)
|
||||
else:
|
||||
return module.fail_json(msg="Invalid state %s" % state)
|
||||
except pynetbox.RequestError as e:
|
||||
return module.fail_json(msg=json.loads(e.error))
|
||||
except ValueError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def _check_and_adapt_data(nb, data):
|
||||
data = find_ids(nb, data)
|
||||
|
||||
if data.get("vrf") and not isinstance(data["vrf"], int):
|
||||
raise ValueError(
|
||||
"%s does not exist - Please create VRF" % (data["vrf"])
|
||||
)
|
||||
if data.get("status"):
|
||||
data["status"] = IP_ADDRESS_STATUS.get(data["status"].lower())
|
||||
if data.get("role"):
|
||||
data["role"] = IP_ADDRESS_ROLE.get(data["role"].lower())
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _handle_state_new_present(module, state, nb_app, nb_endpoint, data):
|
||||
if data.get("address"):
|
||||
if state == "present":
|
||||
return module.exit_json(
|
||||
**ensure_ip_address_present(nb_endpoint, data)
|
||||
)
|
||||
elif state == "new":
|
||||
return module.exit_json(
|
||||
**create_ip_address(nb_endpoint, data)
|
||||
)
|
||||
else:
|
||||
if state == "present":
|
||||
return module.exit_json(
|
||||
**ensure_ip_in_prefix_present_on_netif(
|
||||
nb_app, nb_endpoint, data
|
||||
)
|
||||
)
|
||||
elif state == "new":
|
||||
return module.exit_json(
|
||||
**get_new_available_ip_address(nb_app, data)
|
||||
)
|
||||
|
||||
|
||||
def ensure_ip_address_present(nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(ip_address, msg, changed): dictionary resulting of the request,
|
||||
where 'ip_address' is the serialized ip fetched or newly created in Netbox
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
try:
|
||||
nb_addr = _search_ip(nb_endpoint, data)
|
||||
except ValueError:
|
||||
return _error_multiple_ip_results(data)
|
||||
|
||||
result = {}
|
||||
if not nb_addr:
|
||||
return create_ip_address(nb_endpoint, data)
|
||||
else:
|
||||
ip_addr, diff = update_netbox_object(nb_addr, data, module.check_mode)
|
||||
if ip_addr is False:
|
||||
module.fail_json(
|
||||
msg="Request failed, couldn't update IP: %s" % (data["address"])
|
||||
)
|
||||
if diff:
|
||||
msg = "IP Address %s updated" % (data["address"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
ip_addr = nb_addr.serialize()
|
||||
changed = False
|
||||
msg = "IP Address %s already exists" % (data["address"])
|
||||
|
||||
return {"ip_address": ip_addr, "msg": msg, "changed": changed}
|
||||
|
||||
|
||||
def _search_ip(nb_endpoint, data):
|
||||
get_query_params = {"address": data["address"]}
|
||||
if data.get("vrf"):
|
||||
get_query_params["vrf_id"] = data["vrf"]
|
||||
|
||||
ip_addr = nb_endpoint.get(**get_query_params)
|
||||
return ip_addr
|
||||
|
||||
|
||||
def _error_multiple_ip_results(data):
|
||||
changed = False
|
||||
if "vrf" in data:
|
||||
return {"msg": "Returned more than result", "changed": changed}
|
||||
else:
|
||||
return {
|
||||
"msg": "Returned more than one result - Try specifying VRF.",
|
||||
"changed": changed
|
||||
}
|
||||
|
||||
|
||||
def create_ip_address(nb_endpoint, data):
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
ip_addr, diff = create_netbox_object(nb_endpoint, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "IP Addresses %s created" % (data["address"])
|
||||
|
||||
return {"ip_address": ip_addr, "msg": msg, "changed": changed, "diff": diff}
|
||||
|
||||
|
||||
def ensure_ip_in_prefix_present_on_netif(nb_app, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(ip_address, msg, changed): dictionary resulting of the request,
|
||||
where 'ip_address' is the serialized ip fetched or newly created in Netbox
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
if not data.get("interface") or not data.get("prefix"):
|
||||
raise ValueError("A prefix and interface are required")
|
||||
|
||||
get_query_params = {
|
||||
"interface_id": data["interface"], "parent": data["prefix"],
|
||||
}
|
||||
if data.get("vrf"):
|
||||
get_query_params["vrf_id"] = data["vrf"]
|
||||
|
||||
attached_ips = nb_endpoint.filter(**get_query_params)
|
||||
if attached_ips:
|
||||
ip_addr = attached_ips[-1].serialize()
|
||||
changed = False
|
||||
msg = "IP Address %s already attached" % (ip_addr["address"])
|
||||
|
||||
return {"ip_address": ip_addr, "msg": msg, "changed": changed}
|
||||
else:
|
||||
return get_new_available_ip_address(nb_app, data)
|
||||
|
||||
|
||||
def get_new_available_ip_address(nb_app, data):
|
||||
prefix_query = {"prefix": data["prefix"]}
|
||||
if data.get("vrf"):
|
||||
prefix_query["vrf_id"] = data["vrf"]
|
||||
|
||||
result = {}
|
||||
prefix = nb_app.prefixes.get(**prefix_query)
|
||||
if not prefix:
|
||||
changed = False
|
||||
msg = "%s does not exist - please create first" % (data["prefix"])
|
||||
return {"msg": msg, "changed": changed}
|
||||
elif prefix.available_ips.list():
|
||||
ip_addr, diff = create_netbox_object(prefix.available_ips, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "IP Addresses %s created" % (ip_addr["address"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
changed = False
|
||||
msg = "No available IPs available within %s" % (data['prefix'])
|
||||
return {"msg": msg, "changed": changed}
|
||||
|
||||
result.update({"ip_address": ip_addr, "msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
def _get_prefix_id(nb_app, prefix, vrf_id=None):
|
||||
ipaddr_prefix = ipaddress.ip_network(prefix)
|
||||
network = to_text(ipaddr_prefix.network_address)
|
||||
mask = ipaddr_prefix.prefixlen
|
||||
|
||||
prefix_query_params = {
|
||||
"prefix": network,
|
||||
"mask_length": mask
|
||||
}
|
||||
if vrf_id:
|
||||
prefix_query_params["vrf_id"] = vrf_id
|
||||
|
||||
prefix_id = nb_app.prefixes.get(prefix_query_params)
|
||||
if not prefix_id:
|
||||
if vrf_id:
|
||||
raise ValueError("Prefix %s does not exist in VRF %s - Please create it" % (prefix, vrf_id))
|
||||
else:
|
||||
raise ValueError("Prefix %s does not exist - Please create it" % (prefix))
|
||||
|
||||
return prefix_id
|
||||
|
||||
|
||||
def ensure_ip_address_absent(nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(msg, changed)
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
try:
|
||||
ip_addr = _search_ip(nb_endpoint, data)
|
||||
except ValueError:
|
||||
return _error_multiple_ip_results(data)
|
||||
|
||||
result = {}
|
||||
if ip_addr:
|
||||
dummy, diff = delete_netbox_object(ip_addr, module.check_mode)
|
||||
changed = True
|
||||
msg = "IP Address %s deleted" % (data["address"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
changed = False
|
||||
msg = "IP Address %s already absent" % (data["address"])
|
||||
|
||||
result.update({"msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,461 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>
|
||||
# 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
|
||||
|
||||
ANSIBLE_METADATA = {"metadata_version": "1.1",
|
||||
"status": ["preview"],
|
||||
"supported_by": "community"}
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: netbox_prefix
|
||||
short_description: Creates or removes prefixes from Netbox
|
||||
description:
|
||||
- Creates or removes prefixes from Netbox
|
||||
notes:
|
||||
- Tags should be defined as a YAML list
|
||||
- This should be ran with connection C(local) and hosts C(localhost)
|
||||
author:
|
||||
- Mikhail Yohman (@FragmentedPacket)
|
||||
- Anthony Ruhier (@Anthony25)
|
||||
requirements:
|
||||
- pynetbox
|
||||
version_added: '2.8'
|
||||
options:
|
||||
netbox_url:
|
||||
description:
|
||||
- URL of the Netbox instance resolvable by Ansible control host
|
||||
required: true
|
||||
type: str
|
||||
netbox_token:
|
||||
description:
|
||||
- The token created within Netbox to authorize API access
|
||||
required: true
|
||||
type: str
|
||||
data:
|
||||
description:
|
||||
- Defines the prefix configuration
|
||||
suboptions:
|
||||
family:
|
||||
description:
|
||||
- Specifies which address family the prefix prefix belongs to
|
||||
choices:
|
||||
- 4
|
||||
- 6
|
||||
type: int
|
||||
prefix:
|
||||
description:
|
||||
- Required if state is C(present) and first_available is False. Will allocate or free this prefix.
|
||||
type: str
|
||||
parent:
|
||||
description:
|
||||
- Required if state is C(present) and first_available is C(yes). Will get a new available prefix in this parent prefix.
|
||||
type: str
|
||||
prefix_length:
|
||||
description:
|
||||
- |
|
||||
Required ONLY if state is C(present) and first_available is C(yes).
|
||||
Will get a new available prefix of the given prefix_length in this parent prefix.
|
||||
type: str
|
||||
site:
|
||||
description:
|
||||
- Site that prefix is associated with
|
||||
type: str
|
||||
vrf:
|
||||
description:
|
||||
- VRF that prefix is associated with
|
||||
type: str
|
||||
tenant:
|
||||
description:
|
||||
- The tenant that the prefix will be assigned to
|
||||
type: str
|
||||
vlan:
|
||||
description:
|
||||
- The VLAN the prefix will be assigned to
|
||||
type: dict
|
||||
status:
|
||||
description:
|
||||
- The status of the prefix
|
||||
choices:
|
||||
- Active
|
||||
- Container
|
||||
- Deprecated
|
||||
- Reserved
|
||||
type: str
|
||||
role:
|
||||
description:
|
||||
- The role of the prefix
|
||||
type: str
|
||||
is_pool:
|
||||
description:
|
||||
- All IP Addresses within this prefix are considered usable
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
- The description of the prefix
|
||||
type: str
|
||||
tags:
|
||||
description:
|
||||
- Any tags that the prefix may need to be associated with
|
||||
type: list
|
||||
custom_fields:
|
||||
description:
|
||||
- Must exist in Netbox and in key/value format
|
||||
type: dict
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
first_available:
|
||||
description:
|
||||
- If C(yes) and state C(present), if an parent is given, it will get the
|
||||
first available prefix of the given prefix_length inside the given parent (and
|
||||
vrf, if given).
|
||||
Unused with state C(absent).
|
||||
default: 'no'
|
||||
type: bool
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates.
|
||||
default: "yes"
|
||||
type: bool
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: "Test Netbox prefix module"
|
||||
connection: local
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: Create prefix within Netbox with only required information
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 10.156.0.0/19
|
||||
state: present
|
||||
|
||||
- name: Delete prefix within netbox
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 10.156.0.0/19
|
||||
state: absent
|
||||
|
||||
- name: Create prefix with several specified options
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
family: 4
|
||||
prefix: 10.156.32.0/19
|
||||
site: Test Site
|
||||
vrf: Test VRF
|
||||
tenant: Test Tenant
|
||||
vlan:
|
||||
name: Test VLAN
|
||||
site: Test Site
|
||||
tenant: Test Tenant
|
||||
vlan_group: Test Vlan Group
|
||||
status: Reserved
|
||||
role: Network of care
|
||||
description: Test description
|
||||
is_pool: true
|
||||
tags:
|
||||
- Schnozzberry
|
||||
state: present
|
||||
|
||||
- name: Get a new /24 inside 10.156.0.0/19 within Netbox - Parent doesn't exist
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
parent: 10.156.0.0/19
|
||||
prefix_length: 24
|
||||
state: present
|
||||
first_available: yes
|
||||
|
||||
- name: Create prefix within Netbox with only required information
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
prefix: 10.156.0.0/19
|
||||
state: present
|
||||
|
||||
- name: Get a new /24 inside 10.156.0.0/19 within Netbox
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
parent: 10.156.0.0/19
|
||||
prefix_length: 24
|
||||
state: present
|
||||
first_available: yes
|
||||
|
||||
- name: Get a new /24 inside 10.157.0.0/19 within Netbox with additional values
|
||||
netbox_prefix:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
parent: 10.157.0.0/19
|
||||
prefix_length: 24
|
||||
vrf: Test VRF
|
||||
site: Test Site
|
||||
state: present
|
||||
first_available: yes
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
prefix:
|
||||
description: Serialized object as created or already existent within Netbox
|
||||
returned: on creation
|
||||
type: dict
|
||||
msg:
|
||||
description: Message indicating failure or info about what has been achieved
|
||||
returned: always
|
||||
type: str
|
||||
"""
|
||||
|
||||
import json
|
||||
import traceback
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
find_ids,
|
||||
normalize_data,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
PREFIX_STATUS,
|
||||
)
|
||||
from ansible.module_utils.compat import ipaddress
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
PYNETBOX_IMP_ERR = None
|
||||
try:
|
||||
import pynetbox
|
||||
HAS_PYNETBOX = True
|
||||
except ImportError:
|
||||
PYNETBOX_IMP_ERR = traceback.format_exc()
|
||||
HAS_PYNETBOX = False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
netbox_url=dict(type="str", required=True),
|
||||
netbox_token=dict(type="str", required=True, no_log=True),
|
||||
data=dict(type="dict", required=True),
|
||||
state=dict(required=False, default="present", choices=["present", "absent"]),
|
||||
first_available=dict(type="bool", required=False, default=False),
|
||||
validate_certs=dict(type="bool", default=True),
|
||||
)
|
||||
|
||||
global module
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# Fail module if pynetbox is not installed
|
||||
if not HAS_PYNETBOX:
|
||||
module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR)
|
||||
|
||||
# Assign variables to be used with module
|
||||
app = "ipam"
|
||||
endpoint = "prefixes"
|
||||
url = module.params["netbox_url"]
|
||||
token = module.params["netbox_token"]
|
||||
data = module.params["data"]
|
||||
state = module.params["state"]
|
||||
first_available = module.params["first_available"]
|
||||
validate_certs = module.params["validate_certs"]
|
||||
|
||||
# Attempt to create Netbox API object
|
||||
try:
|
||||
nb = pynetbox.api(url, token=token, ssl_verify=validate_certs)
|
||||
except Exception:
|
||||
module.fail_json(msg="Failed to establish connection to Netbox API")
|
||||
try:
|
||||
nb_app = getattr(nb, app)
|
||||
except AttributeError:
|
||||
module.fail_json(msg="Incorrect application specified: %s" % (app))
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
norm_data = normalize_data(data)
|
||||
try:
|
||||
norm_data = _check_and_adapt_data(nb, norm_data)
|
||||
if "present" in state:
|
||||
return module.exit_json(**ensure_prefix_present(
|
||||
nb, nb_endpoint, norm_data, first_available
|
||||
))
|
||||
else:
|
||||
return module.exit_json(
|
||||
**ensure_prefix_absent(nb, nb_endpoint, norm_data)
|
||||
)
|
||||
except pynetbox.RequestError as e:
|
||||
return module.fail_json(msg=json.loads(e.error))
|
||||
except ValueError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
except AttributeError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def ensure_prefix_present(nb, nb_endpoint, data, first_available=False):
|
||||
"""
|
||||
:returns dict(prefix, msg, changed): dictionary resulting of the request,
|
||||
where 'prefix' is the serialized device fetched or newly created in Netbox
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
if first_available:
|
||||
for k in ("parent", "prefix_length"):
|
||||
if k not in data:
|
||||
raise ValueError("'%s' is required with first_available" % k)
|
||||
|
||||
return get_new_available_prefix(nb_endpoint, data)
|
||||
else:
|
||||
if "prefix" not in data:
|
||||
raise ValueError("'prefix' is required without first_available")
|
||||
|
||||
return get_or_create_prefix(nb_endpoint, data)
|
||||
|
||||
|
||||
def _check_and_adapt_data(nb, data):
|
||||
data = find_ids(nb, data)
|
||||
|
||||
if data.get("vrf") and not isinstance(data["vrf"], int):
|
||||
raise ValueError(
|
||||
"%s does not exist - Please create VRF" % (data["vrf"])
|
||||
)
|
||||
|
||||
if data.get("status"):
|
||||
data["status"] = PREFIX_STATUS.get(data["status"].lower())
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _search_prefix(nb_endpoint, data):
|
||||
if data.get("prefix"):
|
||||
prefix = ipaddress.ip_network(data["prefix"])
|
||||
elif data.get("parent"):
|
||||
prefix = ipaddress.ip_network(data["parent"])
|
||||
|
||||
network = to_text(prefix.network_address)
|
||||
mask = prefix.prefixlen
|
||||
|
||||
if data.get("vrf"):
|
||||
if not isinstance(data["vrf"], int):
|
||||
raise ValueError("%s does not exist - Please create VRF" % (data["vrf"]))
|
||||
else:
|
||||
prefix = nb_endpoint.get(q=network, mask_length=mask, vrf_id=data["vrf"])
|
||||
else:
|
||||
prefix = nb_endpoint.get(q=network, mask_length=mask, vrf="null")
|
||||
|
||||
return prefix
|
||||
|
||||
|
||||
def _error_multiple_prefix_results(data):
|
||||
changed = False
|
||||
|
||||
if data.get("vrf"):
|
||||
return {"msg": "Returned more than one result", "changed": changed}
|
||||
else:
|
||||
return {
|
||||
"msg": "Returned more than one result - Try specifying VRF.",
|
||||
"changed": changed
|
||||
}
|
||||
|
||||
|
||||
def get_or_create_prefix(nb_endpoint, data):
|
||||
try:
|
||||
nb_prefix = _search_prefix(nb_endpoint, data)
|
||||
except ValueError:
|
||||
return _error_multiple_prefix_results(data)
|
||||
|
||||
result = dict()
|
||||
if not nb_prefix:
|
||||
prefix, diff = create_netbox_object(nb_endpoint, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "Prefix %s created" % (prefix["prefix"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
prefix, diff = update_netbox_object(nb_prefix, data, module.check_mode)
|
||||
if prefix is False:
|
||||
module.fail_json(
|
||||
msg="Request failed, couldn't update prefix: %s" % (data["prefix"])
|
||||
)
|
||||
if diff:
|
||||
msg = "Prefix %s updated" % (data["prefix"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Prefix %s already exists" % (data["prefix"])
|
||||
changed = False
|
||||
|
||||
result.update({"prefix": prefix, "msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
def get_new_available_prefix(nb_endpoint, data):
|
||||
try:
|
||||
parent_prefix = _search_prefix(nb_endpoint, data)
|
||||
except ValueError:
|
||||
return _error_multiple_prefix_results(data)
|
||||
|
||||
result = dict()
|
||||
if not parent_prefix:
|
||||
changed = False
|
||||
msg = "Parent prefix does not exist: %s" % (data["parent"])
|
||||
return {"msg": msg, "changed": changed}
|
||||
elif parent_prefix.available_prefixes.list():
|
||||
prefix, diff = create_netbox_object(parent_prefix.available_prefixes, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "Prefix %s created" % (prefix["prefix"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
changed = False
|
||||
msg = "No available prefixes within %s" % (data["parent"])
|
||||
|
||||
result.update({"prefix": prefix, "msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
def ensure_prefix_absent(nb, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(msg, changed)
|
||||
"""
|
||||
try:
|
||||
nb_prefix = _search_prefix(nb_endpoint, data)
|
||||
except ValueError:
|
||||
return _error_multiple_prefix_results(data)
|
||||
|
||||
result = dict()
|
||||
if nb_prefix:
|
||||
dummy, diff = delete_netbox_object(nb_prefix, module.check_mode)
|
||||
changed = True
|
||||
msg = "Prefix %s deleted" % (nb_prefix.prefix)
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Prefix %s already absent" % (data["prefix"])
|
||||
changed = False
|
||||
|
||||
result.update({"msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,348 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>
|
||||
# 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
|
||||
|
||||
ANSIBLE_METADATA = {"metadata_version": "1.1",
|
||||
"status": ["preview"],
|
||||
"supported_by": "community"}
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: netbox_site
|
||||
short_description: Creates or removes sites from Netbox
|
||||
description:
|
||||
- Creates or removes sites from Netbox
|
||||
notes:
|
||||
- Tags should be defined as a YAML list
|
||||
- This should be ran with connection C(local) and hosts C(localhost)
|
||||
author:
|
||||
- Mikhail Yohman (@FragmentedPacket)
|
||||
requirements:
|
||||
- pynetbox
|
||||
version_added: "2.8"
|
||||
options:
|
||||
netbox_url:
|
||||
description:
|
||||
- URL of the Netbox instance resolvable by Ansible control host
|
||||
required: true
|
||||
type: str
|
||||
netbox_token:
|
||||
description:
|
||||
- The token created within Netbox to authorize API access
|
||||
required: true
|
||||
type: str
|
||||
data:
|
||||
description:
|
||||
- Defines the site configuration
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of the site to be created
|
||||
required: true
|
||||
type: str
|
||||
status:
|
||||
description:
|
||||
- Status of the site
|
||||
choices:
|
||||
- Active
|
||||
- Planned
|
||||
- Retired
|
||||
type: str
|
||||
region:
|
||||
description:
|
||||
- The region that the site should be associated with
|
||||
type: str
|
||||
tenant:
|
||||
description:
|
||||
- The tenant the site will be assigned to
|
||||
type: str
|
||||
facility:
|
||||
description:
|
||||
- Data center provider or facility, ex. Equinix NY7
|
||||
type: str
|
||||
asn:
|
||||
description:
|
||||
- The ASN associated with the site
|
||||
type: int
|
||||
time_zone:
|
||||
description:
|
||||
- Timezone associated with the site, ex. America/Denver
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- The description of the prefix
|
||||
type: str
|
||||
physical_address:
|
||||
description:
|
||||
- Physical address of site
|
||||
type: str
|
||||
shipping_address:
|
||||
description:
|
||||
- Shipping address of site
|
||||
type: str
|
||||
latitude:
|
||||
description:
|
||||
- Latitude in decimal format
|
||||
type: int
|
||||
longitude:
|
||||
description:
|
||||
- Longitude in decimal format
|
||||
type: int
|
||||
contact_name:
|
||||
description:
|
||||
- Name of contact for site
|
||||
type: str
|
||||
contact_phone:
|
||||
description:
|
||||
- Contact phone number for site
|
||||
type: str
|
||||
contact_email:
|
||||
description:
|
||||
- Contact email for site
|
||||
type: str
|
||||
comments:
|
||||
description:
|
||||
- Comments for the site. This can be markdown syntax
|
||||
type: str
|
||||
tags:
|
||||
description:
|
||||
- Any tags that the prefix may need to be associated with
|
||||
type: list
|
||||
custom_fields:
|
||||
description:
|
||||
- must exist in Netbox
|
||||
type: dict
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
- |
|
||||
If C(no), SSL certificates will not be validated.
|
||||
This should only be used on personally controlled sites using self-signed certificates.
|
||||
default: "yes"
|
||||
type: bool
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: "Test Netbox site module"
|
||||
connection: local
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Create site within Netbox with only required information
|
||||
netbox_site:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test - Colorado
|
||||
state: present
|
||||
|
||||
- name: Delete site within netbox
|
||||
netbox_site:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test - Colorado
|
||||
state: absent
|
||||
|
||||
- name: Create site with all parameters
|
||||
netbox_site:
|
||||
netbox_url: http://netbox.local
|
||||
netbox_token: thisIsMyToken
|
||||
data:
|
||||
name: Test - California
|
||||
status: Planned
|
||||
region: Test Region
|
||||
tenant: Test Tenant
|
||||
facility: EquinoxCA7
|
||||
asn: 65001
|
||||
time_zone: America/Los Angeles
|
||||
description: This is a test description
|
||||
physical_address: Hollywood, CA, 90210
|
||||
shipping_address: Hollywood, CA, 90210
|
||||
latitude: 10.100000
|
||||
longitude: 12.200000
|
||||
contact_name: Jenny
|
||||
contact_phone: 867-5309
|
||||
contact_email: jenny@changednumber.com
|
||||
comments: ### Placeholder
|
||||
state: present
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
site:
|
||||
description: Serialized object as created or already existent within Netbox
|
||||
returned: on creation
|
||||
type: dict
|
||||
msg:
|
||||
description: Message indicating failure or info about what has been achieved
|
||||
returned: always
|
||||
type: str
|
||||
"""
|
||||
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
find_ids,
|
||||
normalize_data,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
SITE_STATUS,
|
||||
)
|
||||
from ansible.module_utils.compat import ipaddress
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
PYNETBOX_IMP_ERR = None
|
||||
try:
|
||||
import pynetbox
|
||||
HAS_PYNETBOX = True
|
||||
except ImportError:
|
||||
PYNETBOX_IMP_ERR = traceback.format_exc()
|
||||
HAS_PYNETBOX = False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
netbox_url=dict(type="str", required=True),
|
||||
netbox_token=dict(type="str", required=True, no_log=True),
|
||||
data=dict(type="dict", required=True),
|
||||
state=dict(required=False, default="present", choices=["present", "absent"]),
|
||||
validate_certs=dict(type="bool", default=True)
|
||||
)
|
||||
|
||||
global module
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# Fail module if pynetbox is not installed
|
||||
if not HAS_PYNETBOX:
|
||||
module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR)
|
||||
# Assign variables to be used with module
|
||||
app = "dcim"
|
||||
endpoint = "sites"
|
||||
url = module.params["netbox_url"]
|
||||
token = module.params["netbox_token"]
|
||||
data = module.params["data"]
|
||||
state = module.params["state"]
|
||||
validate_certs = module.params["validate_certs"]
|
||||
# Attempt to create Netbox API object
|
||||
try:
|
||||
nb = pynetbox.api(url, token=token, ssl_verify=validate_certs)
|
||||
except Exception:
|
||||
module.fail_json(msg="Failed to establish connection to Netbox API")
|
||||
try:
|
||||
nb_app = getattr(nb, app)
|
||||
except AttributeError:
|
||||
module.fail_json(msg="Incorrect application specified: %s" % (app))
|
||||
nb_endpoint = getattr(nb_app, endpoint)
|
||||
norm_data = normalize_data(data)
|
||||
try:
|
||||
norm_data = _check_and_adapt_data(nb, norm_data)
|
||||
|
||||
if "present" in state:
|
||||
return module.exit_json(
|
||||
**ensure_site_present(nb, nb_endpoint, norm_data)
|
||||
)
|
||||
else:
|
||||
return module.exit_json(
|
||||
**ensure_site_absent(nb, nb_endpoint, norm_data)
|
||||
)
|
||||
except pynetbox.RequestError as e:
|
||||
return module.fail_json(msg=json.loads(e.error))
|
||||
except ValueError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
except AttributeError as e:
|
||||
return module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def _check_and_adapt_data(nb, data):
|
||||
data = find_ids(nb, data)
|
||||
|
||||
if data.get("status"):
|
||||
data["status"] = SITE_STATUS.get(data["status"].lower())
|
||||
|
||||
if "-" in data["name"]:
|
||||
site_slug = data["name"].replace(" ", "").lower()
|
||||
elif " " in data["name"]:
|
||||
site_slug = data["name"].replace(" ", "-").lower()
|
||||
else:
|
||||
site_slug = data["name"].lower()
|
||||
|
||||
data["slug"] = site_slug
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def ensure_site_present(nb, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(interface, msg, changed): dictionary resulting of the request,
|
||||
where 'site' is the serialized interface fetched or newly created in Netbox
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
changed = False
|
||||
return {"msg": data, "changed": changed}
|
||||
|
||||
nb_site = nb_endpoint.get(slug=data["slug"])
|
||||
result = dict()
|
||||
if not nb_site:
|
||||
site, diff = create_netbox_object(nb_endpoint, data, module.check_mode)
|
||||
changed = True
|
||||
msg = "Site %s created" % (data["name"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
site, diff = update_netbox_object(nb_site, data, module.check_mode)
|
||||
if site is False:
|
||||
module.fail_json(
|
||||
msg="Request failed, couldn't update device: %s" % (data["name"])
|
||||
)
|
||||
if diff:
|
||||
msg = "Site %s updated" % (data["name"])
|
||||
changed = True
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Site %s already exists" % (data["name"])
|
||||
changed = False
|
||||
|
||||
result.update({"site": site, "msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
def ensure_site_absent(nb, nb_endpoint, data):
|
||||
"""
|
||||
:returns dict(msg, changed)
|
||||
"""
|
||||
nb_site = nb_endpoint.get(slug=data["slug"])
|
||||
result = dict()
|
||||
if nb_site:
|
||||
dummy, diff = delete_netbox_object(nb_site, module.check_mode)
|
||||
changed = True
|
||||
msg = "Site %s deleted" % (data["name"])
|
||||
result["diff"] = diff
|
||||
else:
|
||||
msg = "Site %s already absent" % (data["name"])
|
||||
changed = False
|
||||
|
||||
result.update({"msg": msg, "changed": changed})
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,499 +0,0 @@
|
||||
# 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)
|
||||
- Anthony Ruhier (@Anthony25)
|
||||
- Nikhil Singh Baliyan (@nikkytub)
|
||||
- Sander Steffann (@steffann)
|
||||
short_description: NetBox inventory source
|
||||
description:
|
||||
- Get inventory hosts from NetBox
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
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
|
||||
config_context:
|
||||
description:
|
||||
- If True, it adds config-context in host vars.
|
||||
- Config-context enables the association of arbitrary data to devices and virtual machines grouped by
|
||||
region, site, role, platform, and/or tenant. Please check official netbox docs for more info.
|
||||
default: False
|
||||
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
|
||||
- tags
|
||||
- device_roles
|
||||
- device_types
|
||||
- manufacturers
|
||||
- platforms
|
||||
default: []
|
||||
query_filters:
|
||||
description: List of parameters passed to the query string (Multiple values may be separated by commas)
|
||||
type: list
|
||||
default: []
|
||||
timeout:
|
||||
description: Timeout for Netbox requests in seconds
|
||||
type: int
|
||||
default: 60
|
||||
compose:
|
||||
description: List of custom ansible host vars to create from the device object fetched from NetBox
|
||||
default: {}
|
||||
type: dict
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# netbox_inventory.yml file in YAML format
|
||||
# Example command line: ansible-inventory -v --list -i netbox_inventory.yml
|
||||
|
||||
plugin: netbox
|
||||
api_endpoint: http://localhost:8000
|
||||
validate_certs: True
|
||||
config_context: False
|
||||
group_by:
|
||||
- device_roles
|
||||
query_filters:
|
||||
- role: network-edge-router
|
||||
|
||||
# Query filters are passed directly as an argument to the fetching queries.
|
||||
# You can repeat tags in the query string.
|
||||
|
||||
query_filters:
|
||||
- role: server
|
||||
- tag: web
|
||||
- tag: production
|
||||
|
||||
# See the NetBox documentation at https://netbox.readthedocs.io/en/latest/api/overview/
|
||||
# the query_filters work as a logical **OR**
|
||||
#
|
||||
# Prefix any custom fields with cf_ and pass the field value with the regular NetBox query string
|
||||
|
||||
query_filters:
|
||||
- cf_foo: bar
|
||||
|
||||
# NetBox inventory plugin also supports Constructable semantics
|
||||
# You can fill your hosts vars using the compose option:
|
||||
|
||||
plugin: netbox
|
||||
compose:
|
||||
foo: last_updated
|
||||
bar: display_name
|
||||
nested_variable: rack.display_name
|
||||
'''
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from sys import version as python_version
|
||||
from threading import Thread
|
||||
from itertools import chain
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
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 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, Constructable, Cacheable):
|
||||
NAME = 'netbox'
|
||||
|
||||
def _fetch_information(self, url):
|
||||
results = None
|
||||
cache_key = self.get_cache_key(url)
|
||||
|
||||
# get the user's cache option to see if we should save the cache if it is changing
|
||||
user_cache_setting = self.get_option('cache')
|
||||
|
||||
# read if the user has caching enabled and the cache isn't being refreshed
|
||||
attempt_to_read_cache = user_cache_setting and self.use_cache
|
||||
|
||||
# attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
|
||||
if attempt_to_read_cache:
|
||||
try:
|
||||
results = self._cache[cache_key]
|
||||
need_to_fetch = False
|
||||
except KeyError:
|
||||
# occurs if the cache_key is not in the cache or if the cache_key expired
|
||||
# we need to fetch the URL now
|
||||
need_to_fetch = True
|
||||
else:
|
||||
# not reading from cache so do fetch
|
||||
need_to_fetch = True
|
||||
|
||||
if need_to_fetch:
|
||||
self.display.v("Fetching: " + url)
|
||||
response = open_url(url, headers=self.headers, timeout=self.timeout, validate_certs=self.validate_certs)
|
||||
|
||||
try:
|
||||
raw_data = to_text(response.read(), errors='surrogate_or_strict')
|
||||
except UnicodeError:
|
||||
raise AnsibleError("Incorrect encoding of fetched payload from NetBox API.")
|
||||
|
||||
try:
|
||||
results = json.loads(raw_data)
|
||||
except ValueError:
|
||||
raise AnsibleError("Incorrect JSON payload: %s" % raw_data)
|
||||
|
||||
# put result in cache if enabled
|
||||
if user_cache_setting:
|
||||
self._cache[cache_key] = results
|
||||
|
||||
return results
|
||||
|
||||
def get_resource_list(self, api_url):
|
||||
"""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.")
|
||||
|
||||
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,
|
||||
"tags": self.extract_tags,
|
||||
"disk": self.extract_disk,
|
||||
"memory": self.extract_memory,
|
||||
"vcpus": self.extract_vcpus,
|
||||
"device_roles": self.extract_device_role,
|
||||
"platforms": self.extract_platform,
|
||||
"device_types": self.extract_device_type,
|
||||
"config_context": self.extract_config_context,
|
||||
"manufacturers": self.extract_manufacturer
|
||||
}
|
||||
|
||||
def extract_disk(self, host):
|
||||
return host.get("disk")
|
||||
|
||||
def extract_vcpus(self, host):
|
||||
return host.get("vcpus")
|
||||
|
||||
def extract_memory(self, host):
|
||||
return host.get("memory")
|
||||
|
||||
def extract_platform(self, host):
|
||||
try:
|
||||
return [self.platforms_lookup[host["platform"]["id"]]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
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:
|
||||
if 'device_role' in host:
|
||||
return [self.device_roles_lookup[host["device_role"]["id"]]]
|
||||
elif 'role' in host:
|
||||
return [self.device_roles_lookup[host["role"]["id"]]]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def extract_config_context(self, host):
|
||||
try:
|
||||
return [host["config_context"]]
|
||||
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 extract_tags(self, host):
|
||||
return host["tags"]
|
||||
|
||||
def refresh_platforms_lookup(self):
|
||||
url = self.api_endpoint + "/api/dcim/platforms/?limit=0"
|
||||
platforms = self.get_resource_list(api_url=url)
|
||||
self.platforms_lookup = dict((platform["id"], platform["name"]) for platform in platforms)
|
||||
|
||||
def refresh_sites_lookup(self):
|
||||
url = 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 = 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 = 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 = 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 = 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 = 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 = 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):
|
||||
lookup_processes = (
|
||||
self.refresh_sites_lookup,
|
||||
self.refresh_regions_lookup,
|
||||
self.refresh_tenants_lookup,
|
||||
self.refresh_racks_lookup,
|
||||
self.refresh_device_roles_lookup,
|
||||
self.refresh_platforms_lookup,
|
||||
self.refresh_device_types_lookup,
|
||||
self.refresh_manufacturers_lookup,
|
||||
)
|
||||
|
||||
thread_list = []
|
||||
for p in lookup_processes:
|
||||
t = Thread(target=p)
|
||||
thread_list.append(t)
|
||||
t.start()
|
||||
|
||||
for thread in thread_list:
|
||||
thread.join()
|
||||
|
||||
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 = tuple(x.keys())[0]
|
||||
v = tuple(x.values())[0]
|
||||
|
||||
if not (k in ALLOWED_DEVICE_QUERY_PARAMETERS or k.startswith("cf_")):
|
||||
msg = "Warning: %s not in %s or starting with cf (Custom field)" % (k, ALLOWED_DEVICE_QUERY_PARAMETERS)
|
||||
self.display.warning(msg=msg)
|
||||
return
|
||||
return k, v
|
||||
|
||||
def refresh_url(self):
|
||||
query_parameters = [("limit", 0)]
|
||||
if self.query_filters:
|
||||
query_parameters.extend(filter(lambda x: x,
|
||||
map(self.validate_query_parameters, self.query_filters)))
|
||||
if self.config_context:
|
||||
self.device_url = self.api_endpoint + "/api/dcim/devices/?" + urlencode(query_parameters)
|
||||
self.virtual_machines_url = self.api_endpoint + "/api/virtualization/virtual-machines/?" + urlencode(query_parameters)
|
||||
else:
|
||||
self.device_url = self.api_endpoint + "/api/dcim/devices/?" + urlencode(query_parameters) + "&exclude=config_context"
|
||||
self.virtual_machines_url = self.api_endpoint + "/api/virtualization/virtual-machines/?" + urlencode(query_parameters) + "&exclude=config_context"
|
||||
|
||||
def fetch_hosts(self):
|
||||
return chain(
|
||||
self.get_resource_list(self.device_url),
|
||||
self.get_resource_list(self.virtual_machines_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 group in self.group_by:
|
||||
sub_groups = self.group_extractors[group](host)
|
||||
|
||||
if not sub_groups:
|
||||
continue
|
||||
|
||||
for sub_group in sub_groups:
|
||||
group_name = "_".join([group, sub_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)
|
||||
|
||||
strict = self.get_option("strict")
|
||||
|
||||
# Composed variables
|
||||
self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict)
|
||||
|
||||
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict)
|
||||
|
||||
# Create groups based on variable values and add the corresponding hosts to it
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict)
|
||||
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)
|
||||
self.use_cache = cache
|
||||
|
||||
# Netbox access
|
||||
token = self.get_option("token")
|
||||
# Handle extra "/" from api_endpoint configuration and trim if necessary, see PR#49943
|
||||
self.api_endpoint = self.get_option("api_endpoint").strip('/')
|
||||
self.timeout = self.get_option("timeout")
|
||||
self.validate_certs = self.get_option("validate_certs")
|
||||
self.config_context = self.get_option("config_context")
|
||||
self.headers = {
|
||||
'Authorization': "Token %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()
|
@ -1,152 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2019, Bruno Inec (@sweenu) <bruno@inec.fr>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
import pytest
|
||||
|
||||
from ansible.module_utils.net_tools.netbox.netbox_utils import (
|
||||
QUERY_TYPES,
|
||||
_build_diff,
|
||||
create_netbox_object,
|
||||
delete_netbox_object,
|
||||
update_netbox_object,
|
||||
normalize_data,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_data():
|
||||
assert "name" not in QUERY_TYPES
|
||||
assert QUERY_TYPES.get("rack") == "slug"
|
||||
assert QUERY_TYPES.get("primary_ip") != "slug"
|
||||
|
||||
raw_data = {
|
||||
"name": "Some name",
|
||||
"primary_ip": "10.3.72.74/31",
|
||||
"rack": "Some rack",
|
||||
}
|
||||
normalized_data = raw_data.copy()
|
||||
normalized_data["rack"] = "some-rack"
|
||||
|
||||
assert normalize_data(raw_data) == normalized_data
|
||||
|
||||
|
||||
def test_build_diff():
|
||||
before = "The state before"
|
||||
after = {"A": "more", "complicated": "state"}
|
||||
diff = _build_diff(before=before, after=after)
|
||||
assert diff == {"before": before, "after": after}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nb_obj_mock(mocker):
|
||||
serialized_object = {"The serialized": "object"}
|
||||
nb_obj = mocker.Mock(name="nb_obj_mock")
|
||||
nb_obj.delete.return_value = True
|
||||
nb_obj.update.return_value = True
|
||||
nb_obj.update.side_effect = serialized_object.update
|
||||
nb_obj.serialize.return_value = serialized_object
|
||||
|
||||
return nb_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def endpoint_mock(mocker, nb_obj_mock):
|
||||
endpoint = mocker.Mock(name="endpoint_mock")
|
||||
endpoint.create.return_value = nb_obj_mock
|
||||
|
||||
return endpoint
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def on_creation_diff():
|
||||
return _build_diff(before={"state": "absent"}, after={"state": "present"})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def on_deletion_diff():
|
||||
return _build_diff(before={"state": "present"}, after={"state": "absent"})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
return {"name": "Some Netbox object name"}
|
||||
|
||||
|
||||
def test_create_netbox_object(endpoint_mock, data, on_creation_diff):
|
||||
return_value = endpoint_mock.create().serialize()
|
||||
|
||||
serialized_obj, diff = create_netbox_object(
|
||||
endpoint_mock, data, check_mode=False
|
||||
)
|
||||
assert endpoint_mock.create.called_once_with(data)
|
||||
assert serialized_obj == return_value
|
||||
assert diff == on_creation_diff
|
||||
|
||||
|
||||
def test_create_netbox_object_in_check_mode(endpoint_mock, data, on_creation_diff):
|
||||
serialized_obj, diff = create_netbox_object(
|
||||
endpoint_mock, data, check_mode=True
|
||||
)
|
||||
assert endpoint_mock.create.not_called()
|
||||
assert serialized_obj == data
|
||||
assert diff == on_creation_diff
|
||||
|
||||
|
||||
def test_delete_netbox_object(nb_obj_mock, on_deletion_diff):
|
||||
serialized_obj, diff = delete_netbox_object(nb_obj_mock, check_mode=False)
|
||||
assert nb_obj_mock.delete.called_once()
|
||||
assert serialized_obj == nb_obj_mock.serialize()
|
||||
assert diff == on_deletion_diff
|
||||
|
||||
|
||||
def test_delete_netbox_object_in_check_mode(nb_obj_mock, on_deletion_diff):
|
||||
serialized_obj, diff = delete_netbox_object(nb_obj_mock, check_mode=True)
|
||||
assert nb_obj_mock.delete.not_called()
|
||||
assert serialized_obj == nb_obj_mock.serialize()
|
||||
assert diff == on_deletion_diff
|
||||
|
||||
|
||||
def test_update_netbox_object_no_changes(nb_obj_mock):
|
||||
unchanged_data = nb_obj_mock.serialize()
|
||||
serialized_obj, diff = update_netbox_object(nb_obj_mock, unchanged_data, check_mode=True)
|
||||
assert nb_obj_mock.update.not_called()
|
||||
assert serialized_obj == unchanged_data
|
||||
assert diff is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def changed_serialized_obj(nb_obj_mock):
|
||||
changed_serialized_obj = nb_obj_mock.serialize().copy()
|
||||
changed_serialized_obj[list(changed_serialized_obj.keys())[0]] += " (modified)"
|
||||
|
||||
return changed_serialized_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def on_update_diff(nb_obj_mock, changed_serialized_obj):
|
||||
return _build_diff(before=nb_obj_mock.serialize().copy(), after=changed_serialized_obj)
|
||||
|
||||
|
||||
def test_update_netbox_object_with_changes(
|
||||
nb_obj_mock, changed_serialized_obj, on_update_diff
|
||||
):
|
||||
serialized_obj, diff = update_netbox_object(
|
||||
nb_obj_mock, changed_serialized_obj, check_mode=False
|
||||
)
|
||||
assert nb_obj_mock.update.called_once_with(changed_serialized_obj)
|
||||
assert serialized_obj == nb_obj_mock.serialize()
|
||||
assert diff == on_update_diff
|
||||
|
||||
|
||||
def test_update_netbox_object_with_changes_in_check_mode(
|
||||
nb_obj_mock, changed_serialized_obj, on_update_diff
|
||||
):
|
||||
updated_serialized_obj = nb_obj_mock.serialize().copy()
|
||||
updated_serialized_obj.update(changed_serialized_obj)
|
||||
|
||||
serialized_obj, diff = update_netbox_object(
|
||||
nb_obj_mock, changed_serialized_obj, check_mode=True
|
||||
)
|
||||
assert nb_obj_mock.update.not_called()
|
||||
|
||||
assert serialized_obj == updated_serialized_obj
|
||||
assert diff == on_update_diff
|
Loading…
Reference in New Issue