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

Add PNG path functionality for IP Fabric v4 #61

Merged
merged 7 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 45 additions & 0 deletions nautobot_chatops_ipfabric/ipfabric.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def get_response_json(self, method, url, payload, params=None):
response = requests.request(method, self.host_url + url, json=payload, params=params, headers=self.headers)
return response.json()

def get_response_raw(self, method, url, payload, params=None):
"""Get request and return response dict."""
headers = {**self.headers}
headers["Accept"] = "*/*"
return requests.request(method, self.host_url + url, json=payload, params=params, headers=headers)

def get_devices_info(self, snapshot_id="$last", limit=DEFAULT_PAGE_LIMIT):
"""Return Device info."""
logger.debug("Received device list request")
Expand All @@ -41,6 +47,13 @@ def get_devices_info(self, snapshot_id="$last", limit=DEFAULT_PAGE_LIMIT):
}
return self.get_response("/api/v1/tables/inventory/devices", payload)

def get_os_version(self):
"""Return Device info."""
logger.debug("Received OS version request")
pke11y marked this conversation as resolved.
Show resolved Hide resolved

payload = {}
return self.get_response_json("GET", "/api/v1/os/version", payload)
pke11y marked this conversation as resolved.
Show resolved Hide resolved

def get_device_inventory(self, search_key, search_value, snapshot_id="$last", limit=DEFAULT_PAGE_LIMIT):
"""Return Device info."""
logger.debug("Received device inventory request")
Expand Down Expand Up @@ -116,6 +129,38 @@ def get_path_simulation(
payload = {}
return self.get_response_json("GET", "/api/v1/graph/end-to-end-path", payload, params)

def get_pathlookup(
self, src_ip, dst_ip, src_port, dst_port, protocol, snapshot_id
): # pylint: disable=too-many-arguments
"""Return pathlookup simulation as PNG output. Requires v4 IP Fabric server."""
payload = {
"snapshot": snapshot_id,
"parameters": {
"type": "pathLookup",
"pathLookupType": "unicast",
"protocol": protocol,
"startingPoint": src_ip,
"startingPort": src_port,
"destinationPoint": dst_ip,
"destinationPort": dst_port,
"groupBy": "siteName",
"networkMode": "true",
"securedPath": "false",
},
}
logger.debug( # pylint: disable=logging-too-many-args
"Received end-to-end PNG path simulation request: ", payload
)

# no params required
params = {}
response = self.get_response_raw("POST", "/api/v1/graphs/png", payload, params=params)

# IP Fabric can potentially return a blank PNG file with status_code 200 when no path exists
if int(response.headers.get("Content-Length")) < 200:
pke11y marked this conversation as resolved.
Show resolved Hide resolved
return None
return response.content

def get_interfaces_errors_info(self, device, snapshot_id="$last", limit=DEFAULT_PAGE_LIMIT):
"""Return bi-directional interface errors info."""
logger.debug("Received interface error counters request")
Expand Down
104 changes: 101 additions & 3 deletions nautobot_chatops_ipfabric/worker.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Worker functions implementing Nautobot "ipfabric" command and subcommands."""
import logging
import tempfile
import os
from datetime import datetime

from django.conf import settings
from django_rq import job
from netutils.ip import is_ip
from nautobot_chatops.choices import CommandStatusChoices
from nautobot_chatops.workers import subcommand_of, handle_subcommands
from .ipfabric import IpFabric
Expand Down Expand Up @@ -349,6 +353,7 @@ def end_to_end_path(
): # pylint: disable=too-many-arguments, too-many-locals
"""Execute end-to-end path simulation between source and target IP address."""
snapshot_id = get_user_snapshot(dispatcher)
sub_cmd = "end-to-end-path"

dialog_list = [
{
Expand Down Expand Up @@ -378,14 +383,14 @@ def end_to_end_path(
]

if not all([src_ip, dst_ip, src_port, dst_port, protocol]):
dispatcher.multi_input_dialog("ipfabric", "end-to-end-path", "Path Simulation", dialog_list)
dispatcher.multi_input_dialog(f"{BASE_CMD}", f"{sub_cmd}", "Path Simulation", dialog_list)
return CommandStatusChoices.STATUS_SUCCEEDED

dispatcher.send_blocks(
[
*dispatcher.command_response_header(
"ipfabric",
"end-to-end-path",
f"{BASE_CMD}",
f"{sub_cmd}",
[
("src_ip", src_ip),
("dst_ip", dst_ip),
Expand Down Expand Up @@ -427,6 +432,99 @@ def end_to_end_path(
return True


@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"]
protocols = [(protocol.upper(), protocol) for protocol in supported_protocols]

dialog_list = [
pke11y marked this conversation as resolved.
Show resolved Hide resolved
{
"type": "text",
"label": "Source IP",
},
{
"type": "text",
"label": "Destination IP",
},
{
"type": "text",
"label": "Source Port",
"default": "1000",
},
{
"type": "text",
"label": "Destination Port",
"default": "22",
},
{
"type": "select",
"label": "Protocol",
"choices": protocols,
"default": protocols[0],
},
]

if not all([src_ip, dst_ip, src_port, dst_port, protocol]):
dispatcher.multi_input_dialog(f"{BASE_CMD}", f"{sub_cmd}", "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 protocol not in supported_protocols:
dispatcher.send_error(f"You've entered an unsupported protocol: {protocol}")
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),
("src_port", src_port),
("dst_port", dst_port),
("protocol", protocol),
],
"Path Lookup",
ipfabric_logo(dispatcher),
),
dispatcher.markdown_block(f"{ipfabric_api.host_url}/diagrams/pathlookup"),
]
)

# only supported in IP Fabric OS version 4.0+
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks to me that we could implement a generic version validation function, as the product evolves, more cases like this will arise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I'd like to get some feedback on how we could approach version management.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feedback from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyone who is currently maintaining a library that is dependent on a specific API/software library and it's versioning. I was hoping there is a preferred approach for this kind of thing. We're not the first to do it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned during the EU call and the suggestion was to drop the old command in the next release and only support v4 in our next release. Makes it easier to maintain and allows users to use two versions. What do you think?

Copy link
Contributor

@chadell chadell Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the approach is to only support latest IPfabric version we could go this way... but this is a question that we will need to answer more times, and not sure if losing support to "old" ipfabric versions is a good strategy (when there are just some differences).
If we opt for the support for only one version support we have to document it well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I guess it's a trade off between maintenance vs functionality. It's harder and more time-consuming to maintain and test multiple versions, especially in our environment. But it's better for users to have greater support.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's not the right strategy for function. But it probably is the right strategy for maintenance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go ahead for now, we can implement it later if needed, before deprectaing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a generic validation function for now. I agree, it'll come in handy.

response = ipfabric_api.get_os_version()
os_version = float(response.get("version", "0.0").rpartition(".")[0])
if os_version >= 4:
Copy link
Contributor

@chadell chadell Jan 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding version handling, I was thinking about another potential approach and I would like to get your feedback.
Your proposal is:

  • if os_version <x -> command_1
  • if os_version >= x -> command_2

what about:

  • command and behind the scenes we check the os_version and decide which logic to use? (this works well if the input data is the same)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm totally open to a better solution. Originally I had the logic in the end-to-end-path command. However the new v4 OS names the path simulation a different name with a different URL. Also, we could extend the new path lookup for JSON and SVG output if desired. I felt it would be adding complexity, for the short term. But perhaps I'm not understanding your proposal?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my idea would be to expose the user with only one command: the_very_best_e2e_path
Inside this we would have a version if statement that would call the specific logic that you need for end-to-end-path and pathlookup (data input if there are changes, and the actual output)
The goal is to make the user experience "unique", so the user don' have to understand there are two implementations, there is one with different behaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'll extend the pathlookup command with JSON capability using an additional argument for output format. Unfortunately the JSON result for the new version and old version are completely different :-(
So the only thing that's similar is the input variables which, will change very soon anyway.

raw_png = ipfabric_api.get_pathlookup(src_ip, dst_ip, src_port, dst_port, protocol, snapshot_id)
if not raw_png:
dispatcher.send_error("An error occurred while retrieving the path lookup")
return CommandStatusChoices.STATUS_FAILED
with tempfile.TemporaryDirectory() as tempdir:
# Note: Microsoft Teams will silently fail if we have ":" in our filename.
pke11y marked this conversation as resolved.
Show resolved Hide resolved
time_str = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

img_path = os.path.join(tempdir, f"{sub_cmd}_{time_str}.png")
with open(img_path, "wb") as img_file:
pke11y marked this conversation as resolved.
Show resolved Hide resolved
img_file.write(raw_png)
dispatcher.send_image(img_path)
else:
dispatcher.send_error(
f"Your IP Fabric OS version {os_version} does not support PNG output. Please try the end-to-end-path command."
)
return CommandStatusChoices.STATUS_FAILED

return True


# ROUTING COMMAND


Expand Down