Netbox_device.py module (#46936)

* netbox_device module

* Add init.py to each directory

* Fixed a few of the shippable failed tests

* No need for import pynetbox in netbox_utils-removed, changed syntax for set

* A bit more cleanup

* Fixed the 'data' to have suboptions

* Fixed formatting for device_role

* Attempting to fix shippable errors

* Final testing and updated documentation

* Fixed return type and removed testing result files

* Updated some returns to be a list to keep 'meta' formatting consistent

* Updated module to standardize the meta return type

* Updated short_description and added David Gomez as author

* Updated short_description, added David Gomez as author, added module direcotry to BOTMETA.yml

* Updated data type to dict and removed JSON from netbox_utils
This commit is contained in:
FragmentedPacket 2018-11-09 00:24:13 -07:00 committed by John R Barker
parent a80c25cbd9
commit 3147dc2a15
5 changed files with 402 additions and 0 deletions

1
.github/BOTMETA.yml vendored
View file

@ -199,6 +199,7 @@ files:
labels:
- networking
- infoblox
$modules/net_tools/netbox/: fragmentedpacket
$modules/network/a10/: ericchou1 mischapeters
$modules/network/aci/: $team_aci
$modules/network/aireos/: jmighion

View file

@ -0,0 +1,144 @@
# -*- 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=['device_roles', 'device_types', 'devices', 'interfaces', 'platforms', 'racks', 'sites'],
extras=[],
ipam=['ip_addresses', 'prefixes', 'vrfs'],
secrets=[],
tenancy=['tenants', 'tenant_groups'],
virtualization=['clusters']
)
QUERY_TYPES = dict(
cluster='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',
site='slug',
tenant='slug',
tenant_group='slug',
vrf='name'
)
CONVERT_TO_ID = dict(
cluster='clusters',
device_role='device_roles',
device_type='device_types',
interface='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',
site='sites',
tenant='tenants',
tenant_group='tenant_groups',
vrf='vrfs'
)
FACE_ID = dict(
front=0,
rear=1
)
NO_DEFAULT_ID = set([
'primary_ip',
'primary_ip4',
'primary_ip6'
])
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
)
def find_app(endpoint):
for k, v in API_APPS_ENDPOINTS.items():
if endpoint in v:
nb_app = k
return nb_app
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)
query_id = nb_endpoint.get(**{QUERY_TYPES.get(k, "q"): search})
if k in NO_DEFAULT_ID:
pass
elif query_id:
data[k] = query_id.id
else:
data[k] = 1
return data
def normalize_data(data):
for k, v in data.items():
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()
return data

View file

@ -0,0 +1,257 @@
#!/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 or delete devices within Netbox
description:
- Creates 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
device_type:
description:
- Required if I(state=present)
device_role:
description:
- Required if I(state=present)
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)
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 (not really required, but helpful)
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
state: absent
- name: Create device with tags
netbox_device:
netbox_url: http://netbox.local
netbox_token: thisIsMyToken
data:
name: Test
device_type: C9410R
device_role: Core Switch
site: Main
tags:
- Schnozzberry
state: present
- name: Create device and assign to rack and position
netbox_device:
netbox_url: http://netbox.local
netbox_token: thisIsMyToken
data:
name: Test
device_type: C9410R
device_role: Core Switch
site: Main
rack: Test Rack
position: 10
face: Front
'''
RETURN = r'''
meta:
description: Message indicating failure or returns results with the object created within Netbox
returned: always
type: list
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.net_tools.netbox.netbox_utils import find_ids, normalize_data, DEVICE_STATUS, FACE_ID
import json
try:
import pynetbox
HAS_PYNETBOX = True
except:
HAS_PYNETBOX = False
def netbox_create_device(nb, nb_endpoint, data):
norm_data = normalize_data(data)
if norm_data.get("status"):
norm_data["status"] = DEVICE_STATUS.get(norm_data["status"].lower(), 0)
if norm_data.get("face"):
norm_data["face"] = FACE_ID.get(norm_data["face"].lower(), 0)
data = find_ids(nb, norm_data)
try:
return nb_endpoint.create([norm_data])
except pynetbox.RequestError as e:
return json.loads(e.error)
def netbox_delete_device(nb_endpoint, data):
norm_data = normalize_data(data)
endpoint = nb_endpoint.get(name=norm_data["name"])
result = []
try:
if endpoint.delete():
result.append({'success': '%s deleted from Netbox' % (norm_data["name"])})
except AttributeError:
result.append({'failed': '%s not found' % (norm_data["name"])})
return result
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)
)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=False)
# Fail module if pynetbox is not installed
if not HAS_PYNETBOX:
module.fail_json(msg='pynetbox is required for this module')
# Assign variables to be used with module
changed = False
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)
if 'present' in state:
response = netbox_create_device(nb, nb_endpoint, data)
if response[0].get('created'):
changed = True
else:
response = netbox_delete_device(nb_endpoint, data)
if 'success' in response[0]:
changed = True
module.exit_json(changed=changed, meta=response)
if __name__ == "__main__":
main()