Skip to content
This repository has been archived by the owner on Sep 7, 2023. It is now read-only.

v5.0 Changes #109

Merged
merged 8 commits into from Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ An IP Fabric ChatOps plugin for [Nautobot](https://github.com/nautobot/nautobot)

This plugin uses the [Nautobot ChatOps](https://github.com/nautobot/nautobot-plugin-chatops/) base framework. It provides the ability to query data from IP Fabric using a supported chat platform (currently Slack, Webex Teams, MS Teams, and Mattermost).

## Version Matrix

Here is a compatibility matrix and the minimum versions required to run this plugin:

| IP Fabric | Python | Nautobot | chatops | chatops-ipfabric | [python-ipfabric](https://github.com/community-fabric/python-ipfabric) | [python-ipfabric-diagrams](https://github.com/community-fabric/python-ipfabric-diagrams) |
|-----------|--------|----------|---------|------------------|------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| 4.4 | 3.7.1 | 1.1.0 | 1.1.0 | 1.2.0 | 0.11.0 | 1.2.7 |
| 5.0.1 | 3.7.1 | 1.1.0 | 1.1.0 | 1.3.0 | 5.0.4 | 5.0.2 |

## Screenshots

![image](https://user-images.githubusercontent.com/29293048/138304572-46d2fa11-8dd2-4722-9ab0-450e20a657a5.png)
Expand Down
12 changes: 1 addition & 11 deletions nautobot_chatops_ipfabric/ipfabric_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ class IpFabric:

EMPTY = "(empty)"

# URLs
INVENTORY_DEVICES_URL = "tables/inventory/devices"
INTERFACE_LOAD_URL = "tables/interfaces/load"
INTERFACE_ERRORS_URL = "tables/interfaces/errors/bidirectional"
INTERFACE_DROPS_URL = "tables/interfaces/drops/bidirectional"
BGP_NEIGHBORS_URL = "tables/routing/protocols/bgp/neighbors"
WIRELESS_SSID_URL = "tables/wireless/radio"
WIRELESS_CLIENT_URL = "tables/wireless/clients"
ADDRESSING_HOSTS_URL = "tables/addressing/hosts"

# COLUMNS
INVENTORY_COLUMNS = [
"hostname",
Expand All @@ -44,7 +34,7 @@ class IpFabric:
"loginIp",
]
DEVICE_INFO_COLUMNS = ["hostname", "siteName", "vendor", "platform", "model"]
INTERFACE_LOAD_COLUMNS = ["intName", "inBytes", "outBytes"]
INTERFACE_LOAD_COLUMNS = ["intName", "bytes", "pkts"]
INTERFACE_ERRORS_COLUMNS = ["intName", "errPktsPct", "errRate"]
INTERFACE_DROPS_COLUMNS = ["intName", "dropsPktsPct", "dropsRate"]
BGP_NEIGHBORS_COLUMNS = [
Expand Down
196 changes: 128 additions & 68 deletions nautobot_chatops_ipfabric/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import tempfile
import os
from datetime import datetime
from operator import ge

from django.conf import settings
from django_rq import job
from nautobot_chatops.choices import CommandStatusChoices
from nautobot_chatops.workers import subcommand_of, handle_subcommands
from netutils.ip import is_ip
from netutils.mac import is_valid_mac
from ipfabric_diagrams import Unicast
from ipfabric_diagrams import Unicast, icmp
from pkg_resources import parse_version

from .ipfabric_wrapper import IpFabric

Expand Down Expand Up @@ -61,6 +61,7 @@ def ipfabric(subcommand, **kwargs):
def prompt_snapshot_id(action_id, help_text, dispatcher, choices=None):
"""Prompt the user for snapshot ID."""
formatted_snapshots = ipfabric_api.get_formatted_snapshots()
get_snapshots_table(dispatcher, formatted_snapshots)
choices = list(formatted_snapshots.values())
default = choices[0]
dispatcher.prompt_from_menu(action_id, help_text, choices, default=default)
Expand All @@ -70,8 +71,7 @@ def prompt_snapshot_id(action_id, help_text, dispatcher, choices=None):
def prompt_inventory_filter_values(action_id, help_text, dispatcher, filter_key, choices=None):
"""Prompt the user for input inventory search value selection."""
column_name = inventory_field_mapping.get(filter_key.lower())
inventory_data = ipfabric_api.client.fetch(
IpFabric.INVENTORY_DEVICES_URL,
inventory_data = ipfabric_api.client.inventory.devices.fetch(
columns=IpFabric.DEVICE_INFO_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=get_user_snapshot(dispatcher),
Expand Down Expand Up @@ -112,6 +112,7 @@ def get_user_snapshot(dispatcher):
def get_snapshots_table(dispatcher, formatted_snapshots=None):
"""IP Fabric Loaded Snapshot list."""
sub_cmd = "get-loaded-snapshots"
ipfabric_api.client.update()
snapshot_table = ipfabric_api.get_snapshots_table(formatted_snapshots)

dispatcher.send_blocks(
Expand Down Expand Up @@ -204,8 +205,7 @@ def get_inventory(dispatcher, filter_key=None, filter_value=None):

col_name = inventory_field_mapping.get(filter_key.lower())
filter_api = {col_name: [IpFabric.IEQ, filter_value]}
devices = ipfabric_api.client.fetch(
IpFabric.INVENTORY_DEVICES_URL,
devices = ipfabric_api.client.inventory.devices.fetch(
columns=IpFabric.INVENTORY_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -245,8 +245,7 @@ def interfaces(dispatcher, device=None, metric=None):
snapshot_id = get_user_snapshot(dispatcher)
logger.debug("Getting devices")
sub_cmd = "interfaces"
inventory_data = ipfabric_api.client.fetch(
IpFabric.INVENTORY_DEVICES_URL,
inventory_data = ipfabric_api.client.inventory.devices.fetch(
columns=IpFabric.DEVICE_INFO_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=get_user_snapshot(dispatcher),
Expand Down Expand Up @@ -303,8 +302,7 @@ def get_int_load(dispatcher, device, snapshot_id):
sub_cmd = "interfaces"
dispatcher.send_markdown(f"Load in interfaces for *{device}* in snapshot *{snapshot_id}*.")
filter_api = {"hostname": [IpFabric.IEQ, device]}
int_load = ipfabric_api.client.fetch(
IpFabric.INTERFACE_LOAD_URL,
int_load = ipfabric_api.client.technology.interfaces.current_rates_data_bidirectional.fetch(
columns=IpFabric.INTERFACE_LOAD_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand All @@ -320,12 +318,12 @@ def get_int_load(dispatcher, device, snapshot_id):
"interface load data",
ipfabric_logo(dispatcher),
),
dispatcher.markdown_block(f"{str(ipfabric_api.ui_url)}technology/interfaces/rate/inbound"),
dispatcher.markdown_block(f"{str(ipfabric_api.ui_url)}technology/interfaces/rate/bidirectional"),
]
)

dispatcher.send_large_table(
["IntName", "IN bps", "OUT bps"],
["IntName", "Bytes/Sec", "Packets/Sec"],
[
[
interface.get(IpFabric.INTERFACE_LOAD_COLUMNS[i], IpFabric.EMPTY)
Expand All @@ -344,8 +342,7 @@ def get_int_errors(dispatcher, device, snapshot_id):
sub_cmd = "interfaces"
dispatcher.send_markdown(f"Load in interfaces for *{device}* in snapshot *{snapshot_id}*.")
filter_api = {"hostname": [IpFabric.IEQ, device]}
int_errors = ipfabric_api.client.fetch(
IpFabric.INTERFACE_ERRORS_URL,
int_errors = ipfabric_api.client.technology.interfaces.average_rates_errors_bidirectional.fetch(
columns=IpFabric.INTERFACE_ERRORS_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -386,8 +383,7 @@ def get_int_drops(dispatcher, device, snapshot_id):
sub_cmd = "interfaces"
dispatcher.send_markdown(f"Load in interfaces for *{device}* in snapshot *{snapshot_id}*.")
filter_api = {"hostname": [IpFabric.IEQ, device]}
int_drops = ipfabric_api.client.fetch(
IpFabric.INTERFACE_DROPS_URL,
int_drops = ipfabric_api.client.technology.interfaces.average_rates_drops_bidirectional.fetch(
columns=IpFabric.INTERFACE_DROPS_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -424,16 +420,67 @@ def get_int_drops(dispatcher, device, snapshot_id):


# PATH LOOKUP COMMMAND
def submit_pathlookup(
dispatcher, sub_cmd, src_ip, dst_ip, protocol, src_port=None, dst_port=None, icmp_type=None
): # pylint: disable=too-many-arguments, too-many-locals
"""Path simulation diagram lookup between source and target IP address."""
snapshot_id = get_user_snapshot(dispatcher)
# diagrams for 4.0 - 4.2 are not supported due to attribute changes in 4.3+
try:
os_version = ipfabric_api.client.os_version
if os_version and os_version >= parse_version("4.3"):
if protocol != "icmp":
unicast = Unicast(
startingPoint=src_ip,
destinationPoint=dst_ip,
protocol=protocol,
srcPorts=src_port,
dstPorts=dst_port,
)
else:
unicast = Unicast(
startingPoint=src_ip,
destinationPoint=dst_ip,
protocol=protocol,
icmp=getattr(icmp, icmp_type),
)
raw_png = ipfabric_api.diagram.diagram_png(unicast, snapshot_id)
if not raw_png:
raise RuntimeError(
"An error occurred while retrieving the path lookup. Please verify the path using the link above."
)
with tempfile.TemporaryDirectory() as tempdir:
# Note: Microsoft Teams will silently fail if we have ":" in our filename, so the timestamp has to skip them.
time_str = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
img_path = os.path.join(tempdir, f"{sub_cmd}_{time_str}.png")
# MS Teams requires permission to upload files.
if dispatcher.needs_permission_to_send_image():
dispatcher.ask_permission_to_send_image(
f"{sub_cmd}_{time_str}.png",
f"{BASE_CMD} {sub_cmd} {src_ip} {dst_ip} {src_port} {dst_port} {protocol}",
)
return False

with open(img_path, "wb") as img_file:
img_file.write(raw_png)
dispatcher.send_image(img_path)
return CommandStatusChoices.STATUS_SUCCEEDED
else:
raise RuntimeError(
f"Diagrams only supported in IP Fabric version 4.3+ and current version is {str(ipfabric_api.client.os_version)}"
)
except (RuntimeError, OSError) as error:
dispatcher.send_error(error)
return CommandStatusChoices.STATUS_FAILED


@subcommand_of("ipfabric")
def pathlookup(
dispatcher, src_ip, dst_ip, src_port, dst_port, protocol
): # pylint: disable=too-many-arguments, too-many-locals
"""Path simulation diagram lookup between source and target IP address."""
snapshot_id = get_user_snapshot(dispatcher)
sub_cmd = "pathlookup"
supported_protocols = ["tcp", "udp", "icmp"]
supported_protocols = ["tcp", "udp"]
protocols = [(protocol.upper(), protocol) for protocol in supported_protocols]

# identical to dialog_list in end-to-end-path; consolidate dialog_list if maintaining both cmds
Expand Down Expand Up @@ -495,44 +542,64 @@ def pathlookup(
]
)

# diagrams for 4.0 - 4.2 are not supported due to attribute changes in 4.3+
try:
os_version = ipfabric_api.client.os_version
if os_version and ge(os_version, "4.3"):
unicast = Unicast(
startingPoint=src_ip,
destinationPoint=dst_ip,
protocol=protocol,
srcPorts=src_port,
dstPorts=dst_port,
)
raw_png = ipfabric_api.diagram.diagram_png(unicast, snapshot_id)
if not raw_png:
raise RuntimeError(
"An error occurred while retrieving the path lookup. Please verify the path using the link above."
)
with tempfile.TemporaryDirectory() as tempdir:
# Note: Microsoft Teams will silently fail if we have ":" in our filename, so the timestamp has to skip them.
time_str = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
img_path = os.path.join(tempdir, f"{sub_cmd}_{time_str}.png")
# MS Teams requires permission to upload files.
if dispatcher.needs_permission_to_send_image():
dispatcher.ask_permission_to_send_image(
f"{sub_cmd}_{time_str}.png",
f"{BASE_CMD} {sub_cmd} {src_ip} {dst_ip} {src_port} {dst_port} {protocol}",
)
return False
submit_pathlookup(dispatcher, sub_cmd, src_ip, dst_ip, protocol, src_port=src_port, dst_port=dst_port)
return True

with open(img_path, "wb") as img_file:
img_file.write(raw_png)
dispatcher.send_image(img_path)
else:
raise RuntimeError(
"PNG output for this chatbot is only supported on IP Fabric version 4.3 and above. Please try the end-to-end-path command."
)
except (RuntimeError, OSError) as error:
dispatcher.send_error(error)

@subcommand_of("ipfabric")
def pathlookup_icmp(dispatcher, src_ip, dst_ip, icmp_type): # pylint: disable=too-many-arguments, too-many-locals
"""Path simulation diagram lookup between source and target IP address."""
sub_cmd = "pathlookup-icmp"
icmp_type = icmp_type.upper() if isinstance(icmp_type, str) else icmp_type

# identical to dialog_list in end-to-end-path; consolidate dialog_list if maintaining both cmds
dialog_list = [
{
"type": "text",
"label": "Source IP",
},
{
"type": "text",
"label": "Destination IP",
},
{
"type": "select",
"label": "ICMP Type",
"choices": icmp.__all__, # pylint: disable=no-member
"default": icmp.__all__[0], # pylint: disable=no-member
},
]

if not all([src_ip, dst_ip, icmp_type]):
dispatcher.multi_input_dialog(f"{BASE_CMD}", f"{sub_cmd}", "ICMP Path Lookup", dialog_list)
return CommandStatusChoices.STATUS_SUCCEEDED

# verify IP address and protocol is valid
if not is_ip(src_ip) or not is_ip(dst_ip):
dispatcher.send_error("You've entered an invalid IP address")
return CommandStatusChoices.STATUS_FAILED
if icmp_type not in icmp.__all__: # pylint: disable=no-member
dispatcher.send_error("You've entered an invalid ICMP Type")
return CommandStatusChoices.STATUS_FAILED

dispatcher.send_blocks(
[
*dispatcher.command_response_header(
f"{BASE_CMD}",
f"{sub_cmd}",
[
("src_ip", src_ip),
("dst_ip", dst_ip),
("icmp_type", icmp_type),
],
"Path Lookup",
ipfabric_logo(dispatcher),
),
dispatcher.markdown_block(f"{ipfabric_api.ui_url}diagrams/pathlookup"),
]
)

submit_pathlookup(dispatcher, sub_cmd, src_ip, dst_ip, "icmp", icmp_type=icmp_type)
return True


Expand All @@ -546,8 +613,7 @@ def routing(dispatcher, device=None, protocol=None, filter_opt=None):
snapshot_id = get_user_snapshot(dispatcher)
logger.debug("Getting devices")

inventory_devices = ipfabric_api.client.fetch(
IpFabric.INVENTORY_DEVICES_URL,
inventory_devices = ipfabric_api.client.inventory.devices.fetch(
columns=IpFabric.DEVICE_INFO_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=get_user_snapshot(dispatcher),
Expand Down Expand Up @@ -615,8 +681,7 @@ def get_bgp_neighbors(dispatcher, device=None, snapshot_id=None, state=None):
else:
filter_api = {"hostname": ["reg", device]}

bgp_neighbors = ipfabric_api.client.fetch(
IpFabric.BGP_NEIGHBORS_URL,
bgp_neighbors = ipfabric_api.client.technology.routing.bgp_neighbors.fetch(
columns=IpFabric.BGP_NEIGHBORS_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -668,8 +733,7 @@ def wireless(dispatcher, option=None, ssid=None):
sub_cmd = "wireless"
snapshot_id = get_user_snapshot(dispatcher)
logger.debug("Getting SSIDs")
ssids = ipfabric_api.client.fetch(
IpFabric.WIRELESS_SSID_URL,
ssids = ipfabric_api.client.technology.wireless.radios_detail.fetch(
columns=IpFabric.WIRELESS_SSID_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=snapshot_id,
Expand Down Expand Up @@ -707,8 +771,7 @@ def wireless(dispatcher, option=None, ssid=None):
def get_wireless_ssids(dispatcher, ssid=None, snapshot_id=None):
"""Get All Wireless SSID Information."""
sub_cmd = "wireless"
ssids = ipfabric_api.client.fetch(
IpFabric.WIRELESS_SSID_URL,
ssids = ipfabric_api.client.technology.wireless.radios_detail.fetch(
columns=IpFabric.WIRELESS_SSID_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=snapshot_id,
Expand Down Expand Up @@ -764,8 +827,7 @@ def get_wireless_ssids(dispatcher, ssid=None, snapshot_id=None):
def get_wireless_clients(dispatcher, ssid=None, snapshot_id=None):
"""Get Wireless Clients."""
sub_cmd = "wireless"
wireless_ssids = ipfabric_api.client.fetch(
IpFabric.WIRELESS_SSID_URL,
wireless_ssids = ipfabric_api.client.technology.wireless.radios_detail.fetch(
columns=IpFabric.WIRELESS_SSID_COLUMNS,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
snapshot_id=snapshot_id,
Expand Down Expand Up @@ -798,8 +860,7 @@ def get_wireless_clients(dispatcher, ssid=None, snapshot_id=None):
return False

filter_api = {"ssid": [IpFabric.IEQ, ssid]} if ssid else {}
clients = ipfabric_api.client.fetch(
IpFabric.WIRELESS_CLIENT_URL,
clients = ipfabric_api.client.technology.wireless.clients.fetch(
columns=IpFabric.WIRELESS_CLIENT_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -867,8 +928,7 @@ def find_host(dispatcher, filter_key=None, filter_value=None):
return CommandStatusChoices.STATUS_FAILED

filter_api = {filter_key: [IpFabric.EQ, filter_value]}
inventory_hosts = ipfabric_api.client.fetch(
IpFabric.ADDRESSING_HOSTS_URL,
inventory_hosts = ipfabric_api.client.inventory.hosts.fetch(
columns=IpFabric.ADDRESSING_HOSTS_COLUMNS,
filters=filter_api,
limit=IpFabric.DEFAULT_PAGE_LIMIT,
Expand Down
Loading