diff --git a/.gitignore b/.gitignore index 00f2bb4..124a1a4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ansible_collections/ .cache/ collections/ envvars +*.gz \ No newline at end of file diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 3e7634b..0000000 --- a/.pylintrc +++ /dev/null @@ -1,111 +0,0 @@ -# The format of this file isn't really documented; just use --generate-rcfile - -[messages control] -# abstract-class-little-used: abstract classes are often useful as interface descriptions -# abstract-class-not-used : No, you just missed it. -# broad-except : seriously, sometimes you need to catch all exceptions -# cyclic-import : detection of this doesn't work well -# duplicate-code : detection of this doesn't work well, mostly catches imports anyway -# fixme : why are you throwing warnings about todos -# incomplete-protocol : this check is just broken. -# interface-not-implemented : No, you just missed it. -# invalid-name : too difficult to configure sanely -# locally-disabled : stupid -# locally-enabled : stupid -# maybe-no-member : if type inference fails, don't warn. -# missing-docstring : docstrings on everything? -# no-self-use : if it were supposed to have been a method, it would be -# star-args : seriously, does no one write wrapper functions -# useless-else-on-loop : detection is broken, doesn't notice "return" statements -# -# Temporarily disabled ones: -# line-too-long -# -# Django generated code: -# bad-continuation -# bad-whitespace -# trailing-whitespace -# old-style-class -# no-init - -disable= - abstract-class-little-used, - abstract-class-not-used, - broad-except, - cyclic-import, - duplicate-code, - fixme, - incomplete-protocol, - interface-not-implemented, - invalid-name, - locally-disabled, - locally-enabled, - maybe-no-member, - missing-docstring, - missing-module-docstring, - no-self-use, - star-args, - useless-else-on-loop, - useless-object-inheritance, - line-too-long, - invalid-name, - bad-continuation, - bad-whitespace, - trailing-whitespace, - old-style-class, - no-init, - len-as-condition, - superfluous-parens, - no-else-raise, - -[basic] -bad-functions=apply,input - -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ -const-rgx=(([A-Z_][A-Za-z0-9_]*)|(__.*__))$ -class-rgx=[A-Za-z_][a-zA-Z0-9_]+$ -function-rgx=[a-zA-Z_][a-zA-Z0-9_]{2,45}$ -method-rgx=[a-z_][a-zA-Z0-9_]{1,80}$ -attr-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$ -argument-rgx=[a-z_][a-z0-9_]{0,30}$ -variable-rgx=[a-z_][a-z0-9_]{0,30}$ -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,50}|(__.*__))$ -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -good-names=i,j,k,ex,Run,_,urlpatterns,application,Input,_Input,HOSTS -bad-names=foo,bar,baz,toto,tutu,tata - -[classes] -defining-attr-methods=__init__,__new__,setUp,initialize - -[design] -# too-many-* and too-few-* warnings are pretty useless, so make them all -# arbitrarily high -max-args=1000 -max-locals=1000 -max-returns=1000 -max-branches=1000 -max-statements=1000 -max-parents=1000 -max-attributes=1000 -max-public-methods=1000 -min-public-methods=0 - -[format] -max-line-length=120 -max-module-lines=100000 - -[imports] -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -[reports] -output-format=text -reports=no - -[typecheck] -ignored-classes=sha1, md5, Popen, Request, SplitResult, execute, API -generated-members=REQUEST,acl_users,aq_parent,^depends$ - -[variables] -additional-builtins=_ -dummy-variables-rgx=_|dummy diff --git a/Makefile b/Makefile index 2870b6e..3000ff8 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ # Makefile +PYTHON_EXE = python3 COLLECTION_NAME="ciscops.mdd" COLLECTION_VERSION := $(shell awk '/^version:/{print $$NF}' galaxy.yml) TARBALL_NAME=ciscops-mdd-${COLLECTION_VERSION}.tar.gz PYDIRS="plugins" +VENV = venv +VENV_BIN=$(VENV)/bin help: ## Display help @awk -F ':|##' \ @@ -12,6 +15,12 @@ help: ## Display help all: test build publish ## Setup python-viptela env and run tests +$(VENV): $(VENV_BIN)/activate ## Build virtual environment + +$(VENV_BIN)/activate: + test -d $(VENV) || $(PYTHON_EXE) -m venv $(VENV) + . $(VENV_BIN)/activate + $(TARBALL_NAME): galaxy.yml @ansible-galaxy collection build @@ -23,10 +32,24 @@ publish: $(TARBALL_NAME) ## Publish Collection (set env:GALAXY_TOKEN) format: ## Format Python code yapf --style=yapf.ini -i -r *.py $(PYDIRS) -test: ## Run Sanity Tests - ansible-test sanity --docker default -v +test: $(VENV) $(TARBALL_NAME) ## Run Sanity Tests + $(RM) -r ./ansible_collections + ansible-galaxy collection install --force $(TARBALL_NAME) -p ./ansible_collections + cd ./ansible_collections/ciscops/mdd && git init . + $(VENV_BIN)/pip uninstall -y ansible-base + $(VENV_BIN)/pip install https://github.com/ansible/ansible/archive/stable-2.10.tar.gz --disable-pip-version-check + cd ./ansible_collections/ciscops/mdd && ../../../$(VENV_BIN)/ansible-test sanity --docker -v --color + $(VENV_BIN)/pip uninstall -y ansible-base + $(VENV_BIN)/pip install https://github.com/ansible/ansible/archive/stable-2.11.tar.gz --disable-pip-version-check + cd ./ansible_collections/ciscops/mdd && ../../../$(VENV_BIN)/ansible-test sanity --docker -v --color + $(VENV_BIN)/pip uninstall -y ansible-base + $(VENV_BIN)/pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check + cd ./ansible_collections/ciscops/mdd && ../../../$(VENV_BIN)/ansible-test sanity --docker -v --color + $(RM) -r ./ansible_collections clean: ## Clean $(RM) $(TARBALL_NAME) + $(RM) -r ./ansible_collections + $(RM) -r ./venv -.PHONY: all clean test build publish +.PHONY: all clean build test publish diff --git a/README.md b/README.md index cf735ea..08c4790 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,11 @@ Example: ansible-playbook nso_init ``` -### `ciscops.mdd.nso_update_device` +### `ciscops.mdd.nso_update_devices` - Update NSO devices from inventory source Example: ``` -ansible-playbook nso_update_netbox +ansible-playbook nso_update_devices ``` diff --git a/galaxy.yml b/galaxy.yml index e91589f..d7a7283 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -61,4 +61,14 @@ issues: https://github.com/model-driven-devops/ansible-mdd/issue/tracker # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', # and '.git' are always filtered -build_ignore: [] +build_ignore: +- '*tar.gz' +- '*.DS_Store' +- '*.json' +- 'venv*' +- '.vscode' +- '.gitignore' +- '.env' +- '.github' +- 'tests/output/' +- 'ansible_collections/' diff --git a/playbooks/cml_update_netbox.yml b/playbooks/cml_update_netbox.yml index 50383c0..25a071d 100644 --- a/playbooks/cml_update_netbox.yml +++ b/playbooks/cml_update_netbox.yml @@ -4,8 +4,12 @@ - ciscops.mdd.netbox tasks: - cisco.cml.cml_lab_facts: - tags: - - network + host: "{{ cml_host }}" + user: "{{ cml_username }}" + password: "{{ cml_password }}" + lab: "{{ cml_lab }}" + # tags: + # - network register: cml_lab_results - include_role: diff --git a/plugins/lookup/netbox_oc.py b/plugins/lookup/netbox_oc.py index 34695b8..afaa4ad 100644 --- a/plugins/lookup/netbox_oc.py +++ b/plugins/lookup/netbox_oc.py @@ -101,6 +101,7 @@ import os import functools from pprint import pformat +from re import search, findall, IGNORECASE from ansible.errors import AnsibleError from ansible.plugins.lookup import LookupBase @@ -251,13 +252,33 @@ def get_endpoint(netbox, resource): return netbox_endpoint_map[resource]["endpoint"] -def get_interface_type(interface_type): - interface_type_map = { - "virtual": "softwareLoopback", - "lag": "ieee8023adLag" - } - if interface_type in interface_type_map: - return interface_type_map[interface_type] +def get_interface_type(interface): + # interface_type_map = { + # "virtual":"softwareLoopback", + # "lag":"ieee8023adLag" + # } + # if interface_type in interface_type_map: + # return interface_type_map[interface_type] + # else: + # return "ethernetCsmacd" + + interface_type = interface["type"]["value"] + + if interface_type == "virtual": + if search("vlan", interface["name"], IGNORECASE): + return "l3ipvlan" + elif search("loopback", interface["name"], IGNORECASE): + return "softwareLoopback" + # If this is a 'dot' subinterface + elif search(r"\.", interface["name"]): + return "ethernetCsmacd" + elif interface_type == "lag": + return "l2vlan" + elif interface["mode"] is not None: + if interface["mode"]["value"] == "tagged": + return "l2vlan" + if interface["mode"]["value"] == "access": + return "l2vlan" else: return "ethernetCsmacd" @@ -293,7 +314,7 @@ def make_netbox_call(nb_endpoint, filters=None): return results -def interfaces_to_oc(interface_data, ipv4_by_intf): +def interfaces_to_oc(interface_data, ipv4_by_intf, fhrp_by_intf): interface_dict = {} vrf_interfaces = {} @@ -311,7 +332,7 @@ def interfaces_to_oc(interface_data, ipv4_by_intf): interface_index = int(interface_name_parts.pop(0)) else: interface_index = 0 - interface_type = get_interface_type(interface["type"]["value"]) + interface_type = get_interface_type(interface) # Use the description if it exists, otherwise, try to # create one @@ -328,98 +349,178 @@ def interfaces_to_oc(interface_data, ipv4_by_intf): # Create the interface list if it is not there if interface_parent not in interface_dict: interface_dict[interface_parent] = { - "config": {}, - "name": interface_parent + "openconfig-interfaces:name": interface_parent, + "openconfig-interfaces:config": {} } # If this is the parent, fill in the parent config if interface_index == 0: - interface_dict[interface_name]["config"] = { - "description": interface_description, - "enabled": interface["enabled"], - "name": interface_name, - "type": interface_type + interface_dict[interface_name]["openconfig-interfaces:config"] = { + "openconfig-interfaces:description": interface_description, + "openconfig-interfaces:enabled": interface["enabled"], + "openconfig-interfaces:name": interface_name, + "openconfig-interfaces:type": interface_type } if interface["mtu"]: - interface_dict[interface_name]["config"]["mtu"] = interface["mtu"] + interface_dict[interface_name]["openconfig-interfaces:config"]["openconfig-interfaces:mtu"] = interface["mtu"] - if interface["count_ipaddresses"] > 0 or interface_index > 0: + if interface_type in ["ethernetCsmacd", "softwareLoopback"] and (interface["count_ipaddresses"] > 0 or interface_index > 0): # This is a Layer 3 interface - # Create the subinterface strcuture if it does not exist - if not interface_dict[interface_parent].get("subinterfaces"): - interface_dict[interface_parent]["subinterfaces"] = { - "subinterface": [] + # Create the subinterface structure if it does not exist + if not interface_dict[interface_parent].get("openconfig-interfaces:subinterfaces"): + interface_dict[interface_parent]["openconfig-interfaces:subinterfaces"] = { + "openconfig-interfaces:subinterface": [] } subinterface = { - "config": { - "description": interface_description, - "enabled": interface["enabled"], - "index": interface_index, - }, - "index": interface_index + "openconfig-interfaces:index": interface_index, + "openconfig-interfaces:config": { + "openconfig-interfaces:description": interface_description, + "openconfig-interfaces:enabled": interface["enabled"], + "openconfig-interfaces:index": interface_index, + } } - # Check to see if an IP address(s) exists for this interface + # Check to see if an IP address(s) exists for this interface if interface["id"] in ipv4_by_intf: subinterface["openconfig-if-ip:ipv4"] = { - "config": { - "dhcp-client": False, - "enabled": True - }, - "addresses": { - "address": [] + "openconfig-if-ip:config": { + "openconfig-if-ip:dhcp-client": False, + "openconfig-if-ip:enabled": True } } for id, value in ipv4_by_intf[interface["id"]].items(): - ip_address, ip_prefix = value["address"].split("/") - address = { - "config": { - "ip": ip_address, - "prefix-length": ip_prefix - }, - "ip": ip_address - } - subinterface["openconfig-if-ip:ipv4"]["addresses"]["address"].append(address) + # If this interface is configured for DHCP, set dhcp-client to True and skip IP address section + if value["status"]["value"] == "dhcp": + subinterface["openconfig-if-ip:ipv4"]["openconfig-if-ip:config"]["openconfig-if-ip:dhcp-client"] = True + else: + subinterface["openconfig-if-ip:ipv4"]["openconfig-if-ip:addresses"] = { + "openconfig-if-ip:address": [] + } + ip_address, ip_prefix = value["address"].split("/") + address = { + "openconfig-if-ip:ip": ip_address, + "openconfig-if-ip:config": { + "openconfig-if-ip:ip": ip_address, + "openconfig-if-ip:prefix-length": ip_prefix + } + + } + if interface["id"] in fhrp_by_intf: + vrrp = { + "openconfig-if-ip:vrrp": { + "openconfig-if-ip:vrrp-group": [] + } + } + for group_id, group in fhrp_by_intf[interface["id"]].items(): + vip, vip_mask = group["ip_addresses"][0]["address"].split("/") + vrrp_group = { + "openconfig-if-ip:virtual-router-id": group["group_id"], + "openconfig-if-ip:config": { + "openconfig-if-ip:priority": group["priority"], + "openconfig-if-ip:virtual-address": [ + vip + ], + "openconfig-if-ip:virtual-router-id": group["group_id"] + } + } + vrrp["openconfig-if-ip:vrrp"]["openconfig-if-ip:vrrp-group"].append(vrrp_group) + address.update(vrrp) + Display().vvvvv(pformat(address)) + subinterface["openconfig-if-ip:ipv4"]["openconfig-if-ip:addresses"]["openconfig-if-ip:address"].append(address) # If this IP address is in a VRF, then we need to contruct a list for later if value["vrf"] is not None: if value["vrf"]["name"] not in vrf_interfaces: vrf_interfaces[value["vrf"]["name"]] = [] vrf_interface = { - "id": interface_name, - "interface": interface_parent, - "subinterface": interface_index + "openconfig-network-instance:id": interface_name, + "openconfig-network-instance:interface": interface_parent, + "openconfig-network-instance:subinterface": interface_index } vrf_interfaces[value["vrf"]["name"]].append(vrf_interface) + if value["status"]["value"] == "dhcp": + subinterface["openconfig-if-ip:ipv4"]["openconfig-if-ip:config"]["openconfig-if-ip:dhcp-client"] = True + if interface["untagged_vlan"] is not None: subinterface["openconfig-vlan:vlan"] = { - "config": { - "vlan-id": interface["untagged_vlan"]["vid"] + "openconfig-vlan:config": { + "openconfig-vlan:vlan-id": interface["untagged_vlan"]["vid"] } } - interface_dict[interface_parent]["subinterfaces"]["subinterface"].append(subinterface) - elif interface["mode"]: - interface_dict[interface_parent]["config"]["type"] = "l2vlan" + interface_dict[interface_parent]["openconfig-interfaces:subinterfaces"]["openconfig-interfaces:subinterface"].append(subinterface) + elif interface_type == "l2vlan": + interface_dict[interface_parent]["openconfig-interfaces:config"]["openconfig-interfaces:type"] = "l2vlan" interface_dict[interface_parent]["openconfig-if-ethernet:ethernet"] = { - "config": {}, + # "openconfig-vlan:config": {}, "openconfig-vlan:switched-vlan": {} } switched_vlan = { - "config": {} + "openconfig-vlan:config": {} } # This is a Layer 2 interface if interface["mode"]["value"] == "access": - switched_vlan["config"]["interface-mode"] = "ACCESS" + switched_vlan["openconfig-vlan:config"]["openconfig-vlan:interface-mode"] = "ACCESS" if interface["untagged_vlan"] is not None: - switched_vlan["config"]["access-vlan"] = interface["untagged_vlan"]["vid"] + switched_vlan["openconfig-vlan:config"]["openconfig-vlan:access-vlan"] = interface["untagged_vlan"]["vid"] if interface["mode"]["value"] == "tagged": - switched_vlan["config"]["interface-mode"] = "TRUNK" + switched_vlan["openconfig-vlan:config"]["openconfig-vlan:interface-mode"] = "TRUNK" if interface["untagged_vlan"] is not None: - switched_vlan["config"]["native-vlan"] = interface["untagged_vlan"]["vid"] + switched_vlan["openconfig-vlan:config"]["openconfig-vlan:native-vlan"] = interface["untagged_vlan"]["vid"] if interface["tagged_vlans"] is not None: allowed_vlans = [] for vlan in interface["tagged_vlans"]: allowed_vlans.append(str(vlan["vid"])) - switched_vlan["config"]["trunk-vlans"] = allowed_vlans + switched_vlan["openconfig-vlan:config"]["openconfig-vlan:trunk-vlans"] = allowed_vlans interface_dict[interface_parent]["openconfig-if-ethernet:ethernet"]["openconfig-vlan:switched-vlan"] = switched_vlan + elif interface_type == "l3ipvlan": + interface_dict[interface_parent]["openconfig-interfaces:config"]["openconfig-interfaces:type"] = "l3ipvlan" + # interface_dict[interface_parent]["openconfig-if-ethernet:ethernet"] = { + # # "openconfig-vlan:config": {}, + # "openconfig-vlan:routed-vlan": {} + # } + routed_vlan = { + "openconfig-vlan:config": {}, + "openconfig-if-ip:ipv4": {} + } + if interface["untagged_vlan"] is not None: + routed_vlan["openconfig-vlan:config"] = { + "vlan": interface["untagged_vlan"]["vid"] + } + else: + vid = findall(r'\d+', interface["name"]) + if vid != "": + routed_vlan["openconfig-vlan:config"] = { + "vlan": vid[0] + } + if interface["id"] in ipv4_by_intf: + routed_vlan["openconfig-if-ip:ipv4"] = { + "openconfig-if-ip:config": { + "openconfig-if-ip:dhcp-client": False, + "openconfig-if-ip:enabled": True + }, + "openconfig-if-ip:addresses": { + "openconfig-if-ip:address": [] + } + } + for id, value in ipv4_by_intf[interface["id"]].items(): + ip_address, ip_prefix = value["address"].split("/") + address = { + "openconfig-if-ip:ip": ip_address, + "openconfig-if-ip:config": { + "openconfig-if-ip:ip": ip_address, + "openconfig-if-ip:prefix-length": ip_prefix + } + } + routed_vlan["openconfig-if-ip:ipv4"]["openconfig-if-ip:addresses"]["openconfig-if-ip:address"].append(address) + # If this IP address is in a VRF, then we need to contruct a list for later + if value["vrf"] is not None: + if value["vrf"]["name"] not in vrf_interfaces: + vrf_interfaces[value["vrf"]["name"]] = [] + vrf_interface = { + "openconfig-network-instance:id": interface_name, + "openconfig-network-instance:interface": interface_parent, + "openconfig-network-instance:subinterface": interface_index + } + vrf_interfaces[value["vrf"]["name"]].append(vrf_interface) + interface_dict[interface_parent]["openconfig-vlan:routed-vlan"] = routed_vlan # Need to take the dict and make it into a list for key, value in interface_dict.items(): oc_interfaces_data["openconfig-interfaces:interfaces"]["openconfig-interfaces:interface"].append(value) @@ -430,25 +531,26 @@ def interfaces_to_oc(interface_data, ipv4_by_intf): } for vrf, interfaces in vrf_interfaces.items(): vrf_instance = { - "name": vrf, - "config": { - "name": vrf, - "type": 'L3VRF', - "enabled": True, - "enabled-address-families": [ - "IPV4" + "openconfig-network-instance:name": vrf, + "openconfig-network-instance:config": { + "openconfig-network-instance:name": vrf, + "openconfig-network-instance:type": 'L3VRF', + "openconfig-network-instance:enabled": True, + "openconfig-network-instance:enabled-address-families": [ + "IPV4", + "IPV6" ] }, - "interfaces": { - "interface": [] + "openconfig-network-instance:interfaces": { + "openconfig-network-instance:interface": [] } } for interface in interfaces: vrf_instance_interfaces = { - "id": interface["id"], - "config": interface + "openconfig-network-instance:id": interface["openconfig-network-instance:id"], + "openconfig-network-instance:config": interface } - vrf_instance["interfaces"]["interface"].append(vrf_instance_interfaces) + vrf_instance["openconfig-network-instance:interfaces"]["openconfig-network-instance:interface"].append(vrf_instance_interfaces) oc_interfaces_data["openconfig-network-instance:network-instances"]["openconfig-network-instance:network-instance"].append(vrf_instance) return oc_interfaces_data @@ -535,7 +637,7 @@ def run(self, terms, variables=None, **kwargs): results.append(data) if resource == "interfaces": - # If we are getting interfaces, we also need to get ip-addresses + # If we are getting interfaces, we also need to get ip-addresses and fhrp-groups ipv4_by_intf = {} ipv6_by_intf = {} ipaddresses_by_id = {} @@ -543,10 +645,15 @@ def run(self, terms, variables=None, **kwargs): ipaddresses = make_netbox_call(endpoint, filters=filter) for ipaddress in ipaddresses: ipaddress = dict(ipaddress) + interface_id = None Display().vvvvv(pformat(ipaddress)) if ipaddress.get("assigned_object_id"): interface_id = ipaddress["assigned_object_id"] ip_id = ipaddress["id"] + elif ipaddress.get("interface"): + interface_id = ipaddress["interface"]["id"] + if interface_id is not None: + ip_id = ipaddress["id"] # We need to copy the ipaddress entry to preserve the original in case caching is used. ipaddress_copy = ipaddress.copy() ipaddresses_by_id[ip_id] = ipaddress_copy @@ -558,6 +665,28 @@ def run(self, terms, variables=None, **kwargs): if interface_id not in ipv4_by_intf: ipv4_by_intf[interface_id] = {} ipv4_by_intf[interface_id][ip_id] = ipaddress_copy - oc_data.update(interfaces_to_oc(results, ipv4_by_intf)) + fhrp_by_intf = {} + endpoint = get_endpoint(netbox, "fhrp-group-assignments") + group_assignments = make_netbox_call(endpoint, filters=filter) + for group_assignment in group_assignments: + group_assignment = dict(group_assignment) + if group_assignment.get("interface_id"): + interface_id = group_assignment["interface_id"] + if interface_id is not None: + group_id = group_assignment["group"]["group_id"] + if interface_id not in fhrp_by_intf: + fhrp_by_intf[interface_id] = {} + fhrp_by_intf[interface_id].update({group_id: {"priority": group_assignment["priority"]}}) + endpoint = get_endpoint(netbox, "fhrp-groups") + fhrp_groups = make_netbox_call(endpoint, filters=filter) + for fhrp_group in fhrp_groups: + fhrp_group = dict(fhrp_group) + if fhrp_group.get("group_id"): + for id in fhrp_by_intf.keys(): + for group_id in fhrp_by_intf[id].keys(): + if fhrp_group["group_id"] == group_id: + fhrp_group_copy = fhrp_group.copy() + fhrp_by_intf[id][group_id].update(fhrp_group_copy) + oc_data.update(interfaces_to_oc(results, ipv4_by_intf, fhrp_by_intf)) return oc_data diff --git a/roles/netbox/tasks/nso_interface.yml b/roles/netbox/tasks/nso_interface.yml index 21f96a9..6077504 100644 --- a/roles/netbox/tasks/nso_interface.yml +++ b/roles/netbox/tasks/nso_interface.yml @@ -46,6 +46,20 @@ vid: "{{ interface_data['encapsulation']['dot1Q']['vlan-id'] }}" site: "{{ netbox_site }}" + - name: Get VLAN + uri: + url: "{{ netbox_api }}/api/ipam/vlans/?vid={{ interface_data['encapsulation']['dot1Q']['vlan-id'] }}" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [200] + method: GET + register: results + + - set_fact: + vlan_count: "{{ results.json.count }}" + - name: Create VLAN netbox.netbox.netbox_vlan: netbox_url: "{{ netbox_api }}" @@ -59,6 +73,7 @@ site: "{{ netbox_site }}" state: present validate_certs: "{{ netbox_validate_certs }}" + when: vlan_count == "0" when: interface_data.encapsulation is defined - name: Set Interface Type @@ -90,7 +105,7 @@ tagged_vlans: "{{ interface_tagged_vlans if interface_tagged_vlans else omit }}" untagged_vlan: "{{ interface_untagged_vlan if interface_untagged_vlan else omit }}" lag: "{{ interface_channel_group if 'channel-group' in interface_data else omit }}" - # mtu: "{{ item.value.mtu | default(omit) }}" + mtu: "{{ interface_data.mtu | default(omit) }}" state: present validate_certs: "{{ netbox_validate_certs }}" register: netbox_interface_results @@ -122,9 +137,9 @@ netbox_token: "{{ netbox_token }}" data: address: "{{ ip_address }}" - # vrf: Test description: "{{ interface_data.description | default(omit) }}" - vrf: "{{ interface_data.vrf.forwarding | default(omit) }}" + # Skip VRF assignment now; uncomment when it is working + # vrf: "{{ interface_data.vrf.forwarding | default(omit) }}" assigned_object: device: "{{ inventory_hostname }}" name: "{{ interface_name }}" @@ -134,4 +149,93 @@ when: interface_data.ip.address is defined and interface_data.ip.address.primary is defined tags: - interface - when: interface_data.ip is defined \ No newline at end of file + when: interface_data.ip is defined + + # The NetBox collection does not currently support FHRP, so use URI module instead +- block: + - name: Get FHRP Groups + uri: + url: "{{ netbox_api }}/api/ipam/fhrp-groups/?group_id={{ interface_data.vrrp[0].id }}" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [200] + method: GET + register: results + + - set_fact: + group_count: "{{ results.json.count }}" + + - set_fact: + group_id: "{{ results.json.results[0].id }}" + when: results.json.results[0].id is defined + + - name: Create FHRP Group + uri: + url: "{{ netbox_api }}/api/ipam/fhrp-groups/" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [201] + method: POST + body_format: json + body: + group_id: "{{ interface_data.vrrp[0].id }}" + protocol: "vrrp2" + when: group_count == "0" + register: results + + - set_fact: + group_id: "{{ results.json.id }}" + when: group_count == "0" + + - name: Create FHRP IP Address + uri: + url: "{{ netbox_api }}/api/ipam/ip-addresses/" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [201] + method: POST + body_format: json + body: + address: "{{ interface_data.vrrp[0].ip.address }}/{{ interface_data.ip.address.primary.mask }}" + assigned_object_type: "ipam.fhrpgroup" + assigned_object_id: "{{ results.json.id }}" + when: group_count == "0" + + - name: Get FHRP Group Assignments + uri: + url: "{{ netbox_api }}/api/ipam/fhrp-group-assignments/?interface_id={{ netbox_interface_results.interface.id }}" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [200] + method: GET + register: results + + - set_fact: + assignments_count: "{{ results.json.count }}" + + - name: Assign FHRP Group to Interface + uri: + url: "{{ netbox_api }}/api/ipam/fhrp-group-assignments/" + headers: + Authorization: "Token {{ netbox_token }}" + force_basic_auth: yes + validate_certs: no + status_code: [201] + method: POST + body_format: json + body: + group: "{{ group_id }}" + interface_type: "dcim.interface" + interface_id: "{{ netbox_interface_results.interface.id }}" + priority: "{{ interface_data.vrrp[0].priority | default(100) }}" + when: assignments_count == "0" + + when: interface_data.vrrp is defined and interface_data.ip is defined diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 84e930f..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -coverage==4.0.3 -flake8==3.4.1 -isort==4.2.15 -mock==1.0.1 -pylint==2.4.4 -pytest==2.8.7 -pytest-cov==2.3.1 -tox==2.6.0 -yapf==0.29.0