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

Commit

Permalink
Merge pull request #115 from nautobot/release-v2.0.0
Browse files Browse the repository at this point in the history
Release v2.0.0
  • Loading branch information
pke11y authored Sep 24, 2022
2 parents a150fd7 + aa6259d commit c6d3a15
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 1,310 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## v2.0.0 - 2022-09-22

## Added

- #109 - Migration to use `python-ipfabric` library as the API client to IP Fabric.
- #103 - New command `get-loaded-snapshots` for greater snapshot details.

## Changed

- Version 2.0.0 of `nautobot-plugin-chatops-ipfabric` only supports IP Fabric version v5.0 and above.

## v1.2.0 - 2022-07-07

## Added
Expand Down
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 @@ -70,8 +70,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 +111,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 +204,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 +244,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 +301,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 +317,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 +341,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 +382,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 +419,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 parse_version(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 +541,65 @@ 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
icmp_types = [(icmp_type_name.upper(), icmp_type_name) for icmp_type_name in icmp.__all__]

# 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_types, # pylint: disable=no-member
"default": icmp_types[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

0 comments on commit c6d3a15

Please sign in to comment.