Giter Site home page Giter Site logo

pynautobot's Introduction



pynautobot

Python API client library for Nautobot.

pynautobot was initially developed as a fork of pynetbox. pynetbox was originally developed by Zach Moody at DigitalOcean and the NetBox Community.

The complete documentation for pynautobot can be found at Read the Docs.

Questions? Comments? Join us in the #nautobot Slack channel on Network to Code!

Installation

You can install via pip or poetry

Using pip

$ pip install pynautobot
...

Using poetry

$ git clone https://github.com/nautobot/pynautobot.git
...
$ pip install poetry
...
$ poetry shell
Virtual environment already activated: /home/user/pynautobot/.venv
$ poetry install
...

Quick Start

A short introduction is provided here; the full documention for pynautobot is at Read the Docs.

To begin, import pynautobot and instantiate an Api object, passing the url and token.

import pynautobot
nautobot = pynautobot.api(
    url="http://localhost:8000",
    token="d6f4e314a5b5fefd164995169f28ae32d987704f",
)

The Api object provides access to the Apps in Nautobot. The Apps provide access to the Models and the field data stored in Nautobot. Pynautobot uses the Endpoint class to represent Models. For example, here is how to access Devices stored in Nautobot:

devices = nautobot.dcim.devices
devices
<pynautobot.core.endpoint.Endpoint object at 0x7fe801e62fa0>

Jobs

Pynautobot provides a specialized Endpoint class to represent the Jobs model. This class is called JobsEndpoint. This extends the Endpoint class by adding the run method so pynautobot can be used to call/execute a job run.

  1. Run from an instance of a job.
>>> gc_backup_job = nautobot.extras.jobs.all()[14]
>>> job_result = gc_backup_job.run()
>>> job_result.result.id
'1838f8bd-440f-434e-9f29-82b46549a31d' # <-- Job Result ID.
  1. Run with Job Inputs
job = nautobot.extras.jobs.all()[7]
job.run(data={"hostname_regex": ".*"})
  1. Run by providing the job id
>>> gc_backup_job = nautobot.extras.jobs.run(class_path=nautobot.extras.jobs.all()[14].id)
>>> gc_backup_job.result.id
'548832dc-e586-4c65-a7c1-a4e799398a3b' # <-- Job Result ID.

Queries

Pynautobot provides several ways to retrieve objects from Nautobot. Only the get() method is shown here. To continue from the example above, the Endpoint object returned will be used to get the device named hq-access-01.

switch = devices.get(name="hq-access-01")

The object returned from the get() method is an implementation of the Record class. This object provides access to the field data from Nautobot.

switch.id
'6929b68d-8f87-4470-8377-e7fdc933a2bb'
switch.name
'hq-access-01'
switch.site
hq

Threading

Pynautobot supports multithreaded calls for .filter() and .all() queries. It is highly recommended you have MAX_PAGE_SIZE in your Nautobot install set to anything except 0 or None. The default value of 1000 is usually a good value to use. To enable threading, add threading=True parameter when instantiating the Api object:

nautobot = pynautobot.api(
    url="http://localhost:8000",
    token="d6f4e314a5b5fefd164995169f28ae32d987704f",
    threading=True,
)

Versioning

Used for Nautobot Rest API versioning. Versioning can be controlled globally by setting api_version on initialization of the API class and/or for a specific request e.g (all(), filter(), get(), create() etc.) by setting an optional api_version parameter.

Global versioning

import pynautobot
nautobot = pynautobot.api(
    url="http://localhost:8000",
    token="d6f4e314a5b5fefd164995169f28ae32d987704f",
    api_version="2.1"
)

Request specific versioning

import pynautobot
nautobot = pynautobot.api(
  url="http://localhost:8000", token="d6f4e314a5b5fefd164995169f28ae32d987704f",
)
tags = nautobot.extras.tags
tags.create(name="Tag", api_version="2.0", content_types=["dcim.device"])
tags.get(api_version="2.1",)

Retry logic

By default, the client will not retry any operation. This behavior can be adjusted via the retries optional parameters. This will only affect HTTP codes: 429, 500, 502, 503, and 504.

Retries

import pynautobot
nautobot = pynautobot.api(
    url="http://localhost:8000",
    token="d6f4e314a5b5fefd164995169f28ae32d987704f",
    retries=3
)

Related projects

Please see our wiki for a list of relevant community projects.

pynautobot's People

Contributors

abringenberg avatar chadell avatar cmsirbu avatar dimaqa avatar fach avatar fragmentedpacket avatar jakubkrysl avatar jdrew82 avatar jeffkala avatar jeremystretch avatar jifox avatar jifoxpa avatar jmcgill298 avatar joewesch avatar jqueuniet avatar jsenecal avatar jvanderaa avatar lamiskin avatar markkuleinio avatar nautics889 avatar nobodyisperfect78 avatar nrnvgh avatar pszulczewski avatar raddessi avatar snaselj avatar timizuoebideri1 avatar tsm1th avatar tyler-8 avatar victorpavlushin avatar vil02 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pynautobot's Issues

Housekeeping: Update Invoke Commands to work as other repos

Our Invoke commands are outdated and are more frustrating than they need to be.

We need to pull in existing ones and convert to ntc-invoke once it's released.

The outcome should be to execute the necessary invoke commands without any additional steps such as defining environment variables, etc. unless overriding defaults.

Wrong custom fields endpoint

With Netbox v2.10 custom fields endpoint changed from _custom_field_choices to custom-fields. Since Nautobot was taken from this version, it is not possible to retrieve custom fields now using pynautobot.

Reproducer:

from pynautobot import api
nb = api(url=NAUTOBOT_URL, token=USER_TOKEN)
cf = nb.extras.custom_choices()

Current result

pynautobot.core.query.RequestError: The requested url: NAUTOBOT_URL/api/extras/_custom_field_choices/ could not be found.

Expected result

list of defined custom fields

500 Server Error whent trying to add memeber to virtual-chassis

When I try to add an existing device in Nuatobot to an existing virtual-chassis via Ansible networktocode.nautobot.device module I get 500 Server Error.
I have tried to do the same via Postman and worked work as expected while with pynautobot I get the above error.

The playbook I am using, was working as expected with a previous version of nautobot/ansible/pynautobot. Unfortunately I am not able to tell on which versions they were.

pynautobot version:

(venv) [root@ou01ap2x pb_nautobot_device_onboarding]# pip3 list | grep pynautobot
pynautobot         1.0.4

pynautobot error:

>>> my_device.virtual_chassis="4ed8258a-60f4-4a47-89eb-c3e513224631"
>>> my_device.vc_position=1
>>> my_device.save()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/response.py", line 386, in save
    if req.patch({i: serialized[i] for i in diff}):
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/query.py", line 350, in patch
    return self._make_call(verb="patch", data=data)
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/query.py", line 226, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 500 Internal Server Error but more specific details were not returned in json. Check the Nautobot Logs or investigate this exception's error attribute.
>>> my_device.vc_position=1
>>> my_device.virtual_chassis="N5K-SRV-AL01"
>>> my_device.vc_position=1
>>> my_device.save()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/response.py", line 386, in save
    if req.patch({i: serialized[i] for i in diff}):
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/query.py", line 350, in patch
    return self._make_call(verb="patch", data=data)
  File "/root/git/pb_nautobot_device_onboarding/venv/lib/python3.6/site-packages/pynautobot/core/query.py", line 226, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 500 Internal Server Error but more specific details were not returned in json. Check the Nautobot Logs or investigate this exception's error attribute.

Looking at the nautobot logs, I can see some error relate to django_prometheus/middleware.py

nautobot log:

10:54:07.325 ERROR   django.request :
  Internal Server Error: /api/dcim/devices/2c1422c9-adf8-426d-9450-43aa976c4714/
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.7/site-packages/django/utils/deprecation.py", line 113, in __call__
    response = self.process_request(request)
  File "/usr/local/lib/python3.7/site-packages/django_prometheus/middleware.py", line 254, in process_request
    content_length = int(request.META.get("CONTENT_LENGTH") or 0)
ValueError: invalid literal for int() with base 10: '53, 53'
[pid: 51|app: 0|req: 204/2258] 10.199.30.3 () {40 vars in 664 bytes} [Wed Feb  2 10:54:07 2022] PATCH /api/dcim/devices/2c1422c9-adf8-426d-9450-43aa976c4714/ => generated 1834 bytes in 2 msecs (HTTP/1.1 500) 8 headers in 245 bytes (1 switches on core 0)

Add method to easily run a Job from pynautobot

it would be useful to have a method to easily start the execution of a Job with pynautobot

Something like that

>>> import pynautobot
>>> nautobot = pynautobot.api( url="https://demo.nautobot.com/", token="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
>>>
>>> job = nautobot.extras.jobs.run(class_path="xxx", commit=False, data=None, schedule=None)

Also, maybe a separate request but it would be nice to have a way to execute a Job and wait for it to finish
Something like that

>>> import pynautobot
>>> nautobot = pynautobot.api( url="https://demo.nautobot.com/", token="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
>>> job_result = nautobot.extras.jobs.run_and_wait(class_path="xxx", commit=False)

Add Object Relationships Option to Returned JSON Attributes

When performing an API request outside of pynautobot using ?include=relationships the relationships created under Extensibility for the object are returned. This information is not included whether get, all, or filter are used, and even if it's cast to a dictionary. There are instances where knowing the object on the other end of the relationship is valuable, such as:

If I'm querying VRFs under IPAM, what VLAN do I have a relationship with (I.E. my iBGP VLAN for MLAG or VPC)?

Pynautobot fails when getting device without a name

Version: 1.0.4

When attempting to get an unnamed device, pynautobot throws a series of errors:

 nb.dcim.devices.get('8d1ff4f5-fa9f-5bef-9ae3-79fb153bd88b')
Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/util/connection.py", line 72, in create_connection
    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/socket.py", line 918, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno 8] nodename nor servname provided, or not known

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1256, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1302, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1251, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1011, in _send_output
    self.send(msg)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 951, in send
    self.connect()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 205, in connect
    conn = self._new_conn()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x10ba5b7c0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/adapters.py", line 440, in send
    resp = conn.urlopen(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 785, in urlopen
    retries = retries.increment(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/util/retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='nautobot', port=8080): Max retries exceeded with url: /api/dcim/devices/8d1ff4f5-fa9f-5bef-9ae3-79fb153bd88b/ (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10ba5b7c0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 211, in __repr__
    return str(self)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 208, in __str__
    return getattr(self, "name", None) or getattr(self, "label", None) or ""
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 187, in __getattr__
    if self.full_details():
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 302, in full_details
    self._parse_values(req.get())
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 295, in get
    return req_all()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 253, in req_all
    req = self._make_call(add_params=add_params)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 211, in _make_call
    req = getattr(self.http_session, verb)(url_override or self.url, headers=headers, params=params, json=data)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 542, in get
    return self.request('GET', url, **kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 529, in request
    resp = self.send(prep, **send_kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 645, in send
    r = adapter.send(request, **kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/adapters.py", line 519, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='nautobot', port=8080): Max retries exceeded with url: /api/dcim/devices/8d1ff4f5-fa9f-5bef-9ae3-79fb153bd88b/ (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10ba5b7c0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))
>>> nb.dcim.devices.filter(site="ld8-pc-broa", name=None)
Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/util/connection.py", line 72, in create_connection
    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/socket.py", line 918, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno 8] nodename nor servname provided, or not known

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1256, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1302, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1251, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 1011, in _send_output
    self.send(msg)
  File "/usr/local/Cellar/[email protected]/3.8.12_1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py", line 951, in send
    self.connect()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 205, in connect
    conn = self._new_conn()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x10ba557f0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/adapters.py", line 440, in send
    resp = conn.urlopen(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/connectionpool.py", line 785, in urlopen
    retries = retries.increment(
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/urllib3/util/retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='nautobot', port=8080): Max retries exceeded with url: /api/dcim/devices/8d1ff4f5-fa9f-5bef-9ae3-79fb153bd88b/ (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10ba557f0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 211, in __repr__
    return str(self)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 208, in __str__
    return getattr(self, "name", None) or getattr(self, "label", None) or ""
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 187, in __getattr__
    if self.full_details():
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/response.py", line 302, in full_details
    self._parse_values(req.get())
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 295, in get
    return req_all()
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 253, in req_all
    req = self._make_call(add_params=add_params)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/pynautobot/core/query.py", line 211, in _make_call
    req = getattr(self.http_session, verb)(url_override or self.url, headers=headers, params=params, json=data)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 542, in get
    return self.request('GET', url, **kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 529, in request
    resp = self.send(prep, **send_kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/sessions.py", line 645, in send
    r = adapter.send(request, **kwargs)
  File "/Users/stcorry/Library/Caches/pypoetry/virtualenvs/network-importer-tLw0PxIN-py3.8/lib/python3.8/site-packages/requests/adapters.py", line 519, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='nautobot', port=8080): Max retries exceeded with url: /api/dcim/devices/8d1ff4f5-fa9f-5bef-9ae3-79fb153bd88b/ (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10ba557f0>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))```

Raise for status option in GraphQL query

Versions

Nautobot: 1.0.0b4
pynautobot: 1.0.2

Problem statement

First time using this SDK and liking it! :)

I do not know how this error is handled on the REST endpoint for this SDK, but when executing a GQL on a non-existing device, the errors are not handled:

In [22]: gql_respone = nautobot.graphql.query(query=GET_DEVICE_DATA_QUERY, variables={"device_id": 'DUMMY'})

In [23]: gql_respone.status_code
Out[23]: 200

In [24]: gql_respone.json
Out[24]: {
    'errors': [{'message': "['โ€œDUMMYโ€ is not a valid UUID.']", 'locations': [{'line': 2, 'column': 3}], 'path': ['device']}],
    'data': {'device': None}
}

# OR

In [25]: gql_respone = nautobot.graphql.query(query=GET_DEVICE_DATA_QUERY, variables={"device_id": 'f2533477-5e9f-4741-b787-70ba36f7e447'})

In [26]: gql_respone.status_code
Out[26]: 200

In [27]: gql_respone.json
Out[27]: {
    'errors': [{'message': 'Device matching query does not exist.', 'locations': [{'line': 2, 'column': 3}], 'path': ['device']}],
    'data': {'device': None}
}

In this example I am trying to collect the device data on the device endpoint using its UUID.

The result is returning a 200 status, which I do not know if is really ok since the data is missing, and the only way to check for an error is to validate the returned output.

Proposal

It would be nice to have a method similar to requests raise_for_status() for well known error like Not valid UUID or Device matching. query does not exist, along with their correct status codes (400 or 404)

Tests do not consistently assert_called_with to confirm correct HTTP requests

Observed while troubleshooting #44 and trying to determine why this breakage wasn't detected in testing. For example:

    @patch(
        "requests.sessions.Session.post", return_value=Response(fixture="dcim/device.json"),
    )
    def test_create(self, *_):
        data = {
            "name": "test-device",
            "site": 1,
            "device_type": 1,
            "device_role": 1,
        }
        ret = nb.devices.create(**data)
        self.assertTrue(ret)

At no point does this test confirm that requests.post is called correctly or as expected. Any call to requests.post will return the fixture response. This is a significant test gap.

Support add_params in EndPoint

There are a few params that would be nice to be able to pass through on the endpoints, for filter() and all() if possible. We should add the ability to exclude config contexts and to include computed fields.

On the config contexts part, it is known that this is an expensive query to make. The nornir-nautobot inventory with REST API is using pynautobot to make this call. This is causing some timeouts at times on larger instances. Would like to have this capability to remove the call to get config contexts.

On the computed fields front, a new parameter was added. This would be good to add into the capabilities as long as params are being edited and looked at in the same light as excluding config contexts.

api_version not sent when updating an object

When an object is updated the api_version is not sent to the server.
Steps to reproduce:

nautobot = pynautobot.api(url, token, api_version="1.3")
tag = nautobot.extras.tags.create(name="tag", content_types=["dcim.interface"], slug="tag")
tag.update({"content_types": ["dcim.device"]})
tag = nautobot.extras.tags.get(id=tag.id)
print(tag.content_types)

Excpected output:

['dcim.device']

Actual output:

['dcim.device', 'dcim.site', 'dcim.rack', 'dcim.cable', 'dcim.powerfeed', 'circuits.circuit', 'ipam.prefix', 'ipam.ipaddress', 'ipam.vlan', 'virtualization.virtualmachine', 'extras.job', 'circuits.circuittermination', 'circuits.provider', 'circuits.providernetwork', 'dcim.consoleport', 'dcim.consoleserverport', 'dcim.devicebay', 'dcim.devicetype', 'dcim.frontport', 'dcim.interface', 'dcim.inventoryitem', 'dcim.poweroutlet', 'dcim.powerpanel', 'dcim.powerport', 'dcim.rackreservation', 'dcim.rearport', 'dcim.virtualchassis', 'ipam.aggregate', 'ipam.routetarget', 'ipam.vrf', 'ipam.service', 'extras.gitrepository', 'extras.secret', 'tenancy.tenant', 'virtualization.cluster', 'virtualization.vminterface', 'nautobot_golden_config.compliancefeature', 'nautobot_golden_config.compliancerule', 'nautobot_golden_config.goldenconfigsetting', 'nautobot_golden_config.goldenconfig', 'nautobot_golden_config.configreplace', 'nautobot_golden_config.configremove', 'nautobot_golden_config.configcompliance']

The api version is sent when the GET request is sent:

GET /api/extras/tags/?name=tag HTTP/1.1
Host: `...
User-Agent: python-requests/2.26.0
Accept-Encoding: gzip, deflate
accept: application/json; version=1.3
Connection: keep-alive
authorization: Token ...
Cookie: ...

But not on the PATCH request:

PATCH /api/extras/tags/427038e9-0d20-46f7-a1d6-4753308fe88b/ HTTP/1.1
Host: ...
User-Agent: python-requests/2.26.0
Accept-Encoding: gzip, deflate
accept: application/json;
Connection: keep-alive
authorization: Token ...
Cookie: ...
Content-Length: 37
Content-Type: application/json

Housekeeping: Update Docs Style

Update documentation to use Mkdocs style and Nautobot branding. The Nautobot plugin cookie contains the correct logos and basic docs structure. Example here of some of the changes made for similar update to nornir-nautobot.

List prefixes broken in 1.1.2

After some testing prefix.available_prefixes.list() returns a list of null values in version 1.1.2. Here is some test code:

import pynautobot

nautobot_url = "https://demo.nautobot.com"
api_token = ""
prefix_id = "08dabdef-26f1-4389-a9d7-4126da74f4ec"

nautobot = pynautobot.api(
    url=nautobot_url,
    token=api_token,
)

prefix = nautobot.ipam.prefixes.get(prefix_id)
print(prefix.available_prefixes.list())

In my testing I found:

  • Python 3.10 Nautobot 1.4.1 pynautobot 1.0.2 through 1.1.1 returns
    [10.35.0.0/16, 10.36.0.0/14, 10.40.0.0/13, 10.48.0.0/12, 10.64.0.0/10, 10.128.0.0/9]

  • Python 3.10 Nautobot 1.4.1 pynautobot 1.1.2 returns
    [, , , , , ]

Add support for Nautobot REST API versioning in pynautobot

We should add a user-friendly option for specifying the Nautobot REST API version to request:

  • for all requests coming from a given api instance (i.e. as an initialization/configuration parameter on the Api class)
  • for a specific request (i.e. as an optional parameter to the create(), get(), all(), etc. Endpoint methods, or (less ideally) as a setting directly on the api.http_session object).

Nautobot uses django-rest-framework's Accept-header versioning, so requesting a particular REST API version is a matter of specifying the appropriate HTTP header, such as Accept: application/json; version=1.3.

Supported API versions by Nautobot version:

Nautobot version REST API version(s)
1.0 1.0
1.1 1.1
1.2 1.2
1.3 1.2 (default), 1.3 (if requested)

Additional references and information:

Enhance get/filter/all function to exclude config_contexts

As per https://nautobot.readthedocs.io/en/latest/rest-api/overview/#excluding-config-contexts,
by default the full config_context is included in any response unless ?exclude=config_context is explicitly set.

I'd like to have the option in pynautobot to set this option for all get and filter requests against the API.

The most beautiful way would be a global setting when instantiating a pynautobot instance, like with threading,
and the possibility to override it for a specific call if needed.

Feature: Removing the restriction on `id` for filter

In nornir-nautobot I tried to restrict the inventory to a single host by filtering by "device id".

This is not possible beacuse pynautobot has a restriction to use id as a filter.

@jvanderaa on slack
And that is definitely on pynautobot setting here. I'll have to see about removing the restriction there.
So the work around at this point would to filter after the inventory.

Please remove the the restriction on id for filter (devices, ...)

pynautobot reqquest error

I am trying to flter devices based on their tags it is working on and off but I am not what is the reason
the environment
nornir 3.1.1 and Nautobot 1.0.3

File "/home/test/pynet_nautobot/lib64/python3.6/site-packages/pynautobot/core/query.py", line 226, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 504 Gateway Time-out but more specific details were not returned in json. Check the Nautobot Logs or investigate this exception's error attribute.

and here is the init file

    nr = InitNornir(
        inventory={
            "plugin": "NautobotInventory",
            "options": {
                "nautobot_url": nb_url,
                "nautobot_token": nb_token,
                "ssl_verify": False,
            },
        },
    )

nb = pynautobot.api(url=nb_url,token=nb_token)
    nb.http_session.verify = False
    
    fd = {}
    smartnet_devices=[]
    for a in filt:
        k, v = a.split(':')
        fd[k] = v
    #ff = F(**fd)
    
    #print(type(ff))
        new_devices = nb.dcim.devices.filter(tag=[f"{v}"])
        smartnet_devices.extend(new_devices)
        #import ipdb ; ipdb.set_trace()
    my_device_list = list(set(smartnet_devices))    
    nr = nr.filter(F(name__any= [f"{items}" for items in my_device_list]))

If I try to run the script couple of items then it works

`custom-fields` and `custom-field-choices` are supposed to be different endpoint

In App class of pynautobot library there is method for retrieving data from '/api/extras/custom-fields/' endpoint. The method has name custom_choices().
And it can make some mess, because Nautobot service actually provide another endpoint "/api/extras/custom-field-choices/". So such naming can be somewhat confusing:
from /api/extras:

Extras API root view

GET /api/extras/


{
    ...
    "custom-field-choices": "http://localhost:8888/api/extras/custom-field-choices/",
    "custom-fields": "http://localhost:8888/api/extras/custom-fields/",
    ...
}

And they return completely different data.

Futhermore, there is no method to get data from "/api/extras/custom-field-choices/". Perhaps it should be implemented.

Housekeeping: Update CI to use supported features

Currently GHA runs are working, but providing a warning.

[tests (3.8, 1.2)](https://github.com/nautobot/pynautobot/actions/runs/3858768023/jobs/6577651134)
Node.js 12 actions are deprecated. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/. Please update the following actions to use Node.js 16: actions/checkout@v2

Should follow suit and update the actions being used to avoid deprecation errors/issues.

Allow for manual pagination

Currently, explicit pagination on the filter() and all() endpoints is explicitly forbidden (ref). This can lead to problems where the default timing plus page size (50 when I tried it against demo.nautobot.com) is too much load for the system, causing timeouts or similar errors to happen. This paging behavior happens here.

My suggestion would be to keep this default behavior, but allow the user to optionally control paging (i.e. pass limit + offset) themselves so they can control their request size + timing with values that work on their system.

Improper list format

Environment

  • Python version: v3.7
  • Nautobot version: v1.3.3

Steps to Reproduce

data_list contains details on the device via webhook including hostname.

  1. in Python:
    nautobot = self.nautobot_obj
    device_hostname = data_list.get('nethostname')
    devices = nautobot.dcim.devices
    device_obj = devices.filter(name=device_hostname)
    interface_list = interfaces.filter(device=device_hostname)

2.run the following in python:
print(type(interface_list))
print(interface_list)

Expected Behavior

<class 'list'>
['Ethernet1/1', 'Ethernet1/2']

Observed Behavior

<class 'list'>
[Ethernet1/1, Ethernet1/2]

'/plugins/' missing from URL on write to plugin endpoint

Changing custom plugin model via API produces following error:

pynautobot.core.query.RequestError: The requested url: https://$SERVER/api/ip_health_check/checks/ec131d77-41ec-4e30-a6ed-180bb6032e5b/ could not be found.

The issue is in URl missing plugins part in the request.

It is easily reproducible by having plugin with custom model n the DB. Let's say you have plugin ip_health_check that stores result: bool and link it to IP. This plugin has registered route with view on checks. The correct URL is $SERVER/api/plugins/ip_health_check/checks/.
The following code should change the result to False and save it to DB, but result is above error.

check = nb.plugins.ip_health_check.checks.filter(ip_address="10.24.50.128")[0]
check.result=False
check.save()

invoke start is failing

I used this configuration with an unmodified develompent/dev.env

export NAUTOBOT_VER=1.4.4
export PYTHON_VER=3.7

When executing invoke start the following traceback is displayed.

 *  Executing task in folder nautobot-plugindev: docker logs --tail 1000 -f 6c66bd41055f812cce1490f6d181d5f9203d5aaadbc891094775a71332a0a021 

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (0s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (3s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (6s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (9s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (12s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (15s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (18s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (21s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (24s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โณ Waiting on DB... (27s / 30s)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 64, in load_plugin
    plugin = importlib.import_module(plugin_name)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'example_plugin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/runner/runner.py", line 120, in settings_callback
    "settings": settings,
  File "/usr/local/lib/python3.7/site-packages/nautobot/core/cli.py", line 176, in _configure_settings
    load_plugins(settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 54, in load_plugins
    load_plugin(plugin_name, settings)
  File "/usr/local/lib/python3.7/site-packages/nautobot/extras/plugins/utils.py", line 70, in load_plugin
    ) from err
nautobot.extras.plugins.exceptions.PluginNotFound: Unable to import plugin example_plugin: Module not found. Check that the plugin module has been installed within the correct Python environment.
โŒ Waited 30s or more for the DB to become ready.

pytest 5.x not working on Mac OS M1 with Python 3.9

I couldn't install pynautobot in dev mode on a Max OS M1 with Python 3.9 because of more-itertools
Not exactly sure what was the issue but it disappeared after upgrading pytest to the latest version 6.2.5

Error when updating permission object including constraints

Environment

  • Python version: 3.9.6
  • Pynautobot version: 1.0.3

Hi,
I found out that when updating a permission object (not a problem when creating the identical object for the first time) containing constraints with a specific JSON string (a list of objects), pynautobot would fail with an exception Exception has occurred: AttributeError object has no attribute "id"

I tried to analyze the issue and it seems that the error is generated within _diff() -> serialize() method because it would treat JSON string as a Record object and expecting id attribute within (which of course cannot be present).

Steps to Reproduce

  1. Let's have a permission object within nautobot
  2. Try to update such an object via pynautobot while having constraints attr containing a "list", e.g. [{"_custom_field_data__owner": "constraint-string-value"}] (valid JSON array of object(s))
  3. Update via pyanutobot fails.

Expected Behavior

Just save the object including constraints.

If you send the same data via a cURL query, it passes and saves without an issue. Example:

curl --location --request PATCH 'https://server/api/users/permissions/cadcf757-da95-46e2-9ef7-d3a08e6115f9/'
--header 'accept: application/json'
--header 'Content-Type: application/json'
--header 'Authorization: Token 123456789'
--data-raw '{"constraints": [{"_custom_field_data__owner": "constraint-string-value"}]}'

I am enclosing a PR (#28) that solves this issue hopefully at the right spot, but of course I don't see into all the depths of nautobot and other relations between models and stuff :-)

JSON CustomField Type unable to serialize Dictionary

Nautobot: 1.3.9 -1.4.1
pynautobot: 1.1.2
Python: 3.8

Observed Behavior:

  • In nautobot 1.3 JSON field type was introduced as an option for CustomFields. This field is set as a pythonic representation of JSON in the UI view.
  • If a custom field is created as a JSON type AND the value of the custom field is a dictionary a ValueError is raised for trying to unpack value as a key in the dictionary when calling serialize from PyNautobot on an instance of a device object.

Expected Behavior:

  • PyNautobot able to serialize a JSON type custom field.

Getting virtual chassis errors with object has no attribute "display_name"

I'm trying to obtain virtual chassis with pynautobot, but it errors with object has no attribute "display_name".

This occurs in both pynautobot 1.0.4 and 1.1.0.

Example code:

nb.dcim.virtual_chassis.all()

Error with 1.0.4:

  File "/usr/local/lib/python3.8/dist-packages/pynautobot/core/response.py", line 211, in __repr__
    return str(self)
  File "/usr/local/lib/python3.8/dist-packages/pynautobot/models/dcim.py", line 151, in __str__
    return self.master.display_name
  File "/usr/local/lib/python3.8/dist-packages/pynautobot/core/response.py", line 192, in __getattr__
    raise AttributeError('object has no attribute "{}"'.format(k))
AttributeError: object has no attribute "display_name"

Error with 1.1.0:

  File "/usr/local/lib/python3.8/dist-packages/pynautobot/core/response.py", line 211, in __repr__
    return str(self)
  File "/usr/local/lib/python3.8/dist-packages/pynautobot/models/dcim.py", line 152, in __str__
    return self.master.display_name
  File "/usr/local/lib/python3.8/dist-packages/pynautobot/core/response.py", line 192, in __getattr__
    raise AttributeError('object has no attribute "{}"'.format(k))
AttributeError: object has no attribute "display_name"

Error when printing Virtual-Chassis object

there is an error when printing a Virtual Chassis Record

print(record)
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/ansible/devel/nautobot-transfer/lib/python3.8/site-packages/pynautobot/models/dcim.py", line 151, in __str__
    return self.master.display_name
AttributeError: 'NoneType' object has no attribute 'display_name'

I could see that the Nautobot API returns the field name instead of display_name

I'll filing a pull request with changes pynautobot/pynautobot/models/dcim.py from

class VirtualChassis(Record):
    def __str__(self):
        return self.master.display_name

to

class VirtualChassis(Record):
    def __str__(self):
        if self.master is None:
            return self.name
        else:
            return self.master.display_name

Update tests

Tasks

Add option to ignore SSL validation

When initializing a pynautobot.api instance with Nautobot, I'd like to be able to ignore SSL validation if possible. This would be helpful specifically for development purposes.

No response when creating records

I'm attempting to take a list of devices from a csv file and import them to my nautobot instance. It's my understanding that I need to create records before I do so. I created this method, for example, to create device roles. (I'm also attempting to create device_types and sites records)
image
I'm receiving no response from the api and I'm wondering if there is something missing from my config object that is required to create a record?

Also, if this is the wrong approach to importing devices, please let me know. Thanks!

AttributeError being raised for prefix object when trying to retrieve tags

In my org we add tags to network prefixes as an identifier, and use those tags as a filter to find prefixes. We then perform different operations based on the combination of tags assigned to the prefixes. I can still use the filter on a get(), which provides me a list of prefixes with the tag, but I can no longer retrieve all tags assigned to the resulting prefixes by accessing the tags attribute of the prefixes.

The code below would previously return a list of tags for prefix_obj.tags in v1.0.4. However, in v1.1.1 the same code raises an AttributeError for the 'prefix' attribute being missing.

>>> import pynautobot
>>> url="https://my.nautobot.net"
>>> token='1234mocktoken5678'
>>> nautobot=pynautobot.api(url=url,token=token)
>>> name='prefix-tag-1'
>>> prefix = '10.0.0.0/26'
>>> network = prefix.split("/")[0]
>>> mask = int(prefix.split("/")[1])
>>> vrf = 'my-vrf-uuid-here'
>>> prefix_obj = nautobot.ipam.prefixes.get(q=network, mask_length=mask, vrf_id=vrf)
>>> type(prefix_obj)
<class 'pynautobot.models.ipam.Prefixes'>
>>> prefix_obj.tags

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/pynautobot/core/response.py", line 216, in __repr__
    return str(self)
  File "/usr/local/lib/python3.7/site-packages/pynautobot/models/ipam.py", line 29, in __str__
    return str(self.prefix)
  File "/usr/local/lib/python3.7/site-packages/pynautobot/core/response.py", line 197, in __getattr__
    raise AttributeError('object has no attribute "{}"'.format(k))
AttributeError: object has no attribute "prefix"

I noticed this change in response.py from #41 which redefined self.default_ret = self.endoint.return_obj. Reverting this appears to resolve my issue, but I don't have enough context as to the intent of the original change so I am not sure if that is the correct course of action.

serialize() on the prefix object, with some data redacted, shows that 'prefix' attribute exists, contrary
to what is indicated by the AttributeError:

>>> prefix_obj.serialize()
{'id': '<object-uuid-here>', 
'url': 'https://my.nautobot.net/api/ipam/prefixes/<uuid>/', 
'family': 4, 'prefix': '10.0.0.0/26', 'site': None, 'vrf': '<redacted>',
'tenant': '<redacted>', 'vlan': None, 'status': 'active', 'role': '<redacted>', 'is_pool': False, 
'description': 'my-prefix-description', 'tags': ['prefix-tag-1'], 'custom_fields': {'my_custom_field': None},
'created': '2022-01-18', 'last_updated': '2022-01-18T23:24:28.624153Z', 'display': '10.0.0.0/26'
}

Unable to Get Computed Fields

New in 1.1.0 is computed fields. There should be an object endpoint to see the computed fields.

 import pynautobot

nautobot = pynautobot.api(url='https://demo.nautobot.com', token='yes')

device1 = nautobot.dcim.devices.all()[0]

dir(device1)

['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattr__',
 '__getattribute__',
 '__getitem__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__key__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__setstate__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_add_cache',
 '_diff',
 '_endpoint_from_url',
 '_full_cache',
 '_init_cache',
 '_parse_values',
 'api',
 'asset_tag',
 'cluster',
 'comments',
 'config_context',
 'created',
 'custom_fields',
 'default_ret',
 'delete',
 'device_role',
 'device_type',
 'display',
 'endpoint',
 'face',
 'full_details',
 'has_details',
 'id',
 'last_updated',
 'local_context_data',
 'local_context_schema',
 'name',
 'napalm',
 'parent_device',
 'platform',
 'position',
 'primary_ip',
 'primary_ip4',
 'primary_ip6',
 'rack',
 'save',
 'serial',
 'serialize',
 'site',
 'status',
 'tags',
 'tenant',
 'update',
 'url',
 'vc_position',
 'vc_priority',
 'virtual_chassis']

I would anticipate that there should be computed fields showing up here in the same way local_context_data does.

Housekeeping: Refactor all duplicated calls to `Request` to a base class with a method

Proposal

I propose that a serious refactor is performed to reduce all of the code duplication in this library predominantly around how Request objects are repeatedly constructed and the code to do so is duplicated all over the library.

This was exposed when we recently began the work on adding support for API versioning by adding the api_version= argument, which requires updating the calls in numerous places when it should be done in ONE and only one place.

Justfication

For example, consider the source for pynautobot.core.api where the Api class has three properties: Api.version vs. Api.openapi vs Api.status. Each of these is making the exact same duplicated call to Request(base=self.base_url, http_session=self.http_session, api_version=self.api_version) and then calling a method on the Request object (get_version(), get_openapi(), and get_status() respectively):

@property
def version(self):
"""Gets the API version of Nautobot.
Can be used to check the Nautobot API version if there are
version-dependent features or syntaxes in the API.
:Returns: Version number as a string.
:Example:
>>> import pynautobot
>>> nb = pynautobot.api(
... 'http://localhost:8000',
... token='d6f4e314a5b5fefd164995169f28ae32d987704f'
... )
>>> nb.version
'1.0'
>>>
"""
version = Request(
base=self.base_url, http_session=self.http_session, api_version=self.api_version
).get_version()
return version
def openapi(self):
"""Returns the OpenAPI spec.
Quick helper function to pull down the entire OpenAPI spec.
:Returns: dict
:Example:
>>> import pynautobot
>>> nb = pynautobot.api(
... 'http://localhost:8000',
... token='d6f4e314a5b5fefd164995169f28ae32d987704f'
... )
>>> nb.openapi()
{...}
>>>
"""
return Request(base=self.base_url, http_session=self.http_session, api_version=self.api_version,).get_openapi()
def status(self):
"""Gets the status information from Nautobot.
Available in Nautobot 2.10.0 or newer.
:Returns: Dictionary as returned by Nautobot.
:Raises: :py:class:`.RequestError` if the request is not successful.
:Example:
>>> pprint.pprint(nb.status())
{'django-version': '3.1.3',
'installed-apps': {'cacheops': '5.0.1',
'debug_toolbar': '3.1.1',
'django_filters': '2.4.0',
'django_prometheus': '2.1.0',
'django_rq': '2.4.0',
'django_tables2': '2.3.3',
'drf_yasg': '1.20.0',
'mptt': '0.11.0',
'rest_framework': '3.12.2',
'taggit': '1.3.0',
'timezone_field': '4.0'},
'nautobot-version': '1.0.0',
'plugins': {},
'python-version': '3.7.3',
'rq-workers-running': 1}
>>>
"""
status = Request(
base=self.base_url, token=self.token, http_session=self.http_session, api_version=self.api_version,
).get_status()
return status

Following the code path to the source for pynautobot.core.query.Request, you can then see that each of these methods ALSO has nearly the exact same code duplicated for each instead of calling a centralized method that already sets the same header value and input parameters:

def get_openapi(self):
""" Gets the OpenAPI Spec """
headers = {
"Content-Type": "application/json;",
}
if self.api_version:
headers["accept"] = f"application/json; version={self.api_version}"
req = self.http_session.get("{}docs/?format=openapi".format(self.normalize_url(self.base)), headers=headers,)
if req.ok:
return req.json()
else:
raise RequestError(req)
def get_version(self):
"""Gets the API version of Nautobot.
Issues a GET request to the base URL to read the API version from the
response headers.
:Raises: RequestError if req.ok returns false.
:Returns: Version number as a string. Empty string if version is not
present in the headers.
"""
headers = {"Content-Type": "application/json;"}
if self.api_version:
headers["accept"] = f"application/json; version={self.api_version}"
req = self.http_session.get(self.normalize_url(self.base), headers=headers,)
if req.ok:
return req.headers.get("API-Version", "")
else:
raise RequestError(req)
def get_status(self):
"""Gets the status from /api/status/ endpoint in Nautobot.
:Returns: Dictionary as returned by Nautobot.
:Raises: RequestError if request is not successful.
"""
headers = {"Content-Type": "application/json;"}
if self.token:
headers["authorization"] = "Token {}".format(self.token)
if self.api_version:
headers["accept"] = f"application/json; version={self.api_version}"
req = self.http_session.get("{}status/".format(self.normalize_url(self.base)), headers=headers,)
if req.ok:
return req.json()
else:
raise RequestError(req)

Longer term this is not a maintainable pattern. There are so many individual calls to create Request instances where instead each of the various Api and Endpoint objects or anything that is behaving as a client to the API should inherit from a common base and call the same underlying method on every request using the same centralized code path.

This will make the code easier to maintain and easier to use across the board and assert that when we want to extend or augment existing functionality to support new API features, they can be done in a single place, and won't require scouring the entire source tree.

Multiple results returned for rear-port-templates wheras the API call returns a single record

I've tried to select a rear-port-template entry with pynautobot.

Nautobot: fbe10eeac2ef (v1.0.1b1)
pynautobot: pynautobot==1.0.2

In debug console I've checked the following:

### Lookup manufacturer
mymanufacturer = nbot.dcim.manufacturers.get(slug="schrack")

mymanufacturer.id
'b6bf84af-4e40-46b4-8ba2-d9f094a9f9f3'

mymanufacturer.slug
'schrack'


###  Lookup device-type
mydevicetype = nbot.dcim.device_types.get(model="LWL-12xLC-FD", 

self.mydevicetype.id
'b363a795-d37c-4449-bc19-f9e94321adc9'

self.mydevicetype.model
'LWL-12xLC-FD'


### Select rear-port-template
nbot.dcim.rear_port_templates.get(name="rear1", device_type_id="b363a795-d37c-4449-bc19-f9e94321adc9")
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/ansible/devel/nautobot-transfer/lib/python3.8/site-packages/pynautobot/core/endpoint.py", line 139, in get
    raise ValueError(
ValueError: get() returned more than one result. Check that the kwarg(s) passed are valid for this endpoint or use filter() or all() instead.

### Use filter 
nbot.dcim.rear_port_templates.filter(name="rear1", device_type_id="b363a795-d37c-4449-bc19-f9e94321adc9")
[rear1, rear1]

### details for first entry that should have been returned with get()
nbot.dcim.rear_port_templates.filter(name="rear1", device_type_id="b363a795-d37c-4449-bc19-f9e94321adc9")[0]["device_type"]
{'display': 'Schrack LWL-12xLC-FD', 'id': 'b363a795-d37c-4449-b...e94321adc9', 'manufacturer': {'display': 'Schrack', 'id': 'b6bf84af-4e40-46b4-8...f094a9f9f3', 'name': 'Schrack', 'slug': 'schrack', 'url': 'http://scvpconf01:80...094a9f9f3/'}, 'model': 'LWL-12xLC-FD', 'slug': 'lwl-12xlc-fd', 'url': 'http://scvpconf01:80...94321adc9/'}

### details for 2nd entry
nbot.dcim.rear_port_templates.filter(name="rear1", device_type_id="b363a795-d37c-4449-bc19-f9e94321adc9")[1]["device_type"]
{'display': 'Schrack LWL-4xLC-FD', 'id': 'fb84cace-178e-403b-8...7c014666cb', 'manufacturer': {'display': 'Schrack', 'id': 'b6bf84af-4e40-46b4-8...f094a9f9f3', 'name': 'Schrack', 'slug': 'schrack', 'url': 'http://scvpconf01:80...094a9f9f3/'}, 'model': 'LWL-4xLC-FD', 'slug': 'lwl-4xlc-fd', 'url': 'http://scvpconf01:80...c014666cb/'}

When using the API with this values, only one record is returned.

image

Documentation for Device Filter

The device filtering demonstration is incorrect.

 dt = nb.dcim.device_types.get(devicetype)
  devices = nb.dcim.devices.filter(device_type=dt.slug)

Generates an error.

Verify Ability To Access Included Items

WIth upcoming relationship content being built out, we need to make sure that we can access the data, such as nautobot/nautobot#2092

This would apply for computed fields as well. If this is already well supported, then let's make sure to document it on the RTD page.

nat_inside/outside don't seem to be available as attributes

In [11]: ip = nb.ipam.ip_addresses.get(address="192.168.1.10/24")

In [12]: ip.nat_inside
Out[12]:

In [13]: ip.serialize()
Out[13]:
{'id': '6845e644-6031-4592-aa4e-1f62a317462c',
 'url': 'http://localhost:8080/api/ipam/ip-addresses/6845e644-6031-4592-aa4e-1f62a317462c/',
 'family': 4,
 'address': '192.168.1.10/24',
 'vrf': None,
 'tenant': None,
 'status': 'active',
 'role': None,
 'assigned_object_type': None,
 'assigned_object_id': None,
 'assigned_object': None,
 'nat_inside': '2cbff972-943f-4862-b9fc-b943ee2066fc',
 'nat_outside': None,
 'dns_name': '',
 'description': '',
 'tags': [],
 'custom_fields': {},
 'created': '2022-04-03',
 'last_updated': '2022-04-17T14:37:25.641707Z',
 'display': '192.168.1.10/24'}

It seems to correlate other FK fields such as tenant.

API URL Endpoint doesn't handle "/"

Re-producable using:

Local Environment

  • python version: 3.10.5
  • pynautobot version: 1.2.1

Nautobot Environment

  • nautobot version: 1.4.6b1
  • data validataion plugin version: 1.0.0

Example

In Nautobot, Create a Regex Rule:

  • Name: Test
  • Enabled: False
  • Content type: dcim.site
  • Field: name
  • Regular expression: ^[A-Z]

In Pynautobot, update the regex rule to Enabled: True

import os
import logging
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
import pynautobot
import requests
url = "http://localhost:8080"
token = "ba21aea2b4f83376c3cfc8b9237660e72040dd83"
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
nautobot = pynautobot.api(url=url, token=token)
endpoint = getattr(nautobot.plugins, "data-validation-engine")
endpoint = getattr(endpoint, "rules/regex")
nautobot.http_session.verify = False
endpoint.url

at this point we see endpoint.url = 'http://localhost:8080/api/plugins/data-validation-engine/rules/regex'

rgs = endpoint.all()
rgs[0].endpoint.url

at this point we see rgs[0].endpoint.url = 'http://localhost:8080/api/plugins/data-validation-engine/rules'

rgs[0].serialize()
rgs[0].enabled = True
rgs[0].save()

error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mattmiller/Documents/playground/env/lib/python3.10/site-packages/pynautobot/core/endpoint.py", line 109, in all
    return response_loader(req.get(), self.return_obj, self)
  File "/Users/mattmiller/Documents/playground/env/lib/python3.10/site-packages/pynautobot/core/query.py", line 326, in get
    return req_all()
  File "/Users/mattmiller/Documents/playground/env/lib/python3.10/site-packages/pynautobot/core/query.py", line 284, in req_all
    req = self._make_call(add_params=add_params)
  File "/Users/mattmiller/Documents/playground/env/lib/python3.10/site-packages/pynautobot/core/query.py", line 257, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The requested url: http://localhost:8080/api/plugins/data-validation-engine/rules/ could not be found.

Links to code

"rules/regex"

pynautobot 1.1.0 Errors when trying to insert a cable record

I'm using pynautobot 1.1.0 with nautobot 1.3.1 and trying to insert a new cable entry but it errors. The same works with pynautobot 1.0.4 (with nautobot 1.3.1)

Example code:

cable_record = {'termination_a_type': 'dcim.interface', 'termination_a_id': source.id, 'termination_b_type': 'dcim.interface', 'termination_b_id': target.id, 'status': 'connected'}

nb.dcim.cables.create(cable_record)

This errors with:

  File "/home/appuser/.local/lib/python3.8/site-packages/pynautobot/core/endpoint.py", line 293, in create
    req = Request(
  File "/home/appuser/.local/lib/python3.8/site-packages/pynautobot/core/query.py", line 339, in post
    return self._make_call(verb="post", data=data)
  File "/home/appuser/.local/lib/python3.8/site-packages/pynautobot/core/query.py", line 241, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 406 Not Acceptable: {'detail': 'Invalid version in "Accept" header. Supported versions are 1.2, 1.3'}

I'm getting up pynautobot with the following:

nautobot = nb_api(url=nb_url, token=nb_token)

I have tried to add api_version="1.3" but it fails with the same.

Doing some debugging if I edit pynautobot/core/query.pyand print the headers in _make_call after if self.api_version: I see the following:

{'Content-Type': 'application/json;', 'authorization': 'Token <token>', 'accept': "application/json; version={'termination_a_type': 'dcim.interface', 'termination_a_id': 'cd80ed0b-96c9-49ca-afa2-78d79a18cfaf', 'termination_b_type': 'dcim.interface', 'termination_b_id': 'e6651be5-16bc-4828-a173-5559ff482309', 'status': 'connected'}"}

So it looks like the headers are getting mangled with the post data.

If I revert to 1.0.4 everything is fine, the headers are normal:

{'Content-Type': 'application/json;', 'authorization': 'Token <token>'}

Custom field choices don't have str interpretation

Casting to str endpoint nb.extras.custom_field_choices results in empty string as it has no name or `label.

Reproducer

  1. Have custom field choice with 3 values.
  2. Run following code:
nb = api(url=URL, token=TOKEN)
print(nb.extras.custom_field_choices.all())
  1. Check output

Expected output
['Value1', 'Value2', 'Value3']
Actual output
[, , ]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.