-
Notifications
You must be signed in to change notification settings - Fork 41
Add vendor lookup endpoint #3354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
6c15ccf
to
ff172ed
Compare
🦙 MegaLinter status: ✅ SUCCESS
See detailed report in MegaLinter reports |
Test results 12 files 12 suites 11m 53s ⏱️ Results for commit fafcba0. ♻️ This comment has been updated with latest results. |
ff172ed
to
5535411
Compare
5535411
to
363a643
Compare
363a643
to
a34ad52
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some adding to the documentation at https://nav.readthedocs.io/en/latest/howto/api_parameters.html#detailed-endpoint-specifications is needed
python/nav/web/api/v1/views.py
Outdated
if not mac: | ||
return Response("Missing MAC address", status=status.HTTP_400_BAD_REQUEST) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leads to already getting a 404 response when just initially accessing the endpoint, if you're using a browser to access the API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to fix this issue honestly ..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just a 200 with the message "No mac address selected"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it so it just returns an empty dict like the POST endpoint does if you give it an empty list
python/nav/web/api/v1/views.py
Outdated
@staticmethod | ||
def post(request): | ||
try: | ||
mac_addresses = json.loads(request.body.decode('utf-8')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I use a browser to access the API and try to post to this endpoint I always get "Invalid JSON"
request.body
looks like this in this case: _content_type=application%2Fjson&_content=%5B%22aa%3Abb%3Acc%3Add%3Aee%3Aff%22%2C+%2211%3A22%3A33%3A44%3A55%3A66%22%5D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe request.data
should be used instead here? Or does that only work if the JSON is a dict?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request.data
works, but behaves differently based on what made the request. Via curl it is automatically converted from JSON to a list (including error messages if the JSON is invalid). So if you send a curl post with a JSON array, request.data will give you a list directly. But with the browseable API request.data
is a QueryDict where the json string is accessible via the _content
key.
Would be nice if the browseable API requsts also got automatically parsed. I imagine ModelViews has some builtin stuff that handles this, so would be nice if that can be hooked into somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a fix specifically so the browseable API works, but its not all that pretty.. basically extra parsing to support this specific case
python/nav/web/api/v1/views.py
Outdated
try: | ||
results = get_vendor_names(mac_addresses) | ||
except ValueError: | ||
return Response("Invalid MAC address", status=status.HTTP_400_BAD_REQUEST) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to here return which one(s) is/are invalid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been added
python/nav/web/api/v1/views.py
Outdated
try: | ||
results = get_vendor_names([mac]) | ||
except ValueError: | ||
return Response("Invalid MAC address", status=status.HTTP_400_BAD_REQUEST) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Response("Invalid MAC address", status=status.HTTP_400_BAD_REQUEST) | |
return Response(f"Invalid MAC address '{mac}'", status=status.HTTP_400_BAD_REQUEST) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done this
@@ -346,6 +350,106 @@ def test_interface_with_last_used_should_be_listable( | |||
assert response.status_code == 200 | |||
|
|||
|
|||
class TestVendorLookupGet: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lines
endpoint = 'vendor'
create_token_endpoint(token, endpoint)
feel like something we can set up here for the whole class instead of repeating it in every test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it so it now uses a fixture
response = api_client.get(ENDPOINTS[endpoint]) | ||
assert response.status_code == 400 | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A vendor-endpoint specific fixture would do, I guess :)
The SQL part looks perfect for @lunkwill42 to have a look at 😁 |
@@ -73,4 +73,5 @@ | |||
name="prefix-usage-detail", | |||
), | |||
re_path(r'^', include(router.urls)), | |||
re_path(r'^vendor/?$', views.VendorLookup.as_view(), name='vendor'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My DRF-fu is rusty (as mentioned in another PR review today). I assume the primary reason the url pattern is added explicitly, rather than through DRF's router-thingy is that this view isn't actually a model view with built-in CRUD-magic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, using the router stuff did not seem to work very well with APIView
python/nav/web/api/v1/views.py
Outdated
cursor.execute( | ||
""" | ||
CREATE TEMPORARY TABLE temp_macaddrs ( | ||
mac macaddr PRIMARY KEY | ||
) | ||
""" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fails spectacularly when I issue two queries in a row. Why?
Because the temporary table is temporary within the current database session. The web server process may re-use the same session until something forces it to be closed/reset. Which means that the next call to this API endpoint may be re-using the same session as the previous call to it - and the temporary table is still defined there.
Traceback (most recent call last):
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 87, in _execute
return self.cursor.execute(sql)
The above exception (relation "temp_macaddrs" already exists
) was the direct cause of the following exception:
File "/home/vscode/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 56, in wrapper_view
return view_func(*args, **kwargs)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/views/generic/base.py", line 104, in view
return self.dispatch(request, *args, **kwargs)
File "/home/vscode/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 515, in dispatch
response = self.handle_exception(exc)
File "/home/vscode/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 475, in handle_exception
self.raise_uncaught_exception(exc)
File "/home/vscode/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 486, in raise_uncaught_exception
raise exc
File "/home/vscode/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 512, in dispatch
response = handler(request, *args, **kwargs)
File "/workspaces/nav/python/nav/web/api/v1/views.py", line 1195, in get
results = get_vendor_names([mac])
File "/usr/local/lib/python3.9/contextlib.py", line 79, in inner
return func(*args, **kwds)
File "/workspaces/nav/python/nav/web/api/v1/views.py", line 1230, in get_vendor_names
cursor.execute(
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 102, in execute
return super().execute(sql, params)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 67, in execute
return self._execute_with_wrappers(
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
return executor(sql, params, many, context)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 89, in _execute
return self.cursor.execute(sql, params)
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/utils.py", line 91, in __exit__
raise dj_exc_value.with_traceback(traceback) from exc_value
File "/home/vscode/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 87, in _execute
return self.cursor.execute(sql)
Exception Type: ProgrammingError at /api/1/vendor
Exception Value: relation "temp_macaddrs" already exists
The temporary table should be explicitly deleted once the query is complete.
Since we're not using an async web server or Django's async stuff, I'm going to assume no other code is using the same session at the same time (otherwise, this might lead to conflicts even when two simultaneous API requests are processed at the same time). You may still want to consider adding a suffix to the temporary table that makes it unique to this specific API request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn, I thought the session was closed and any temporary tables deleted when the function tagged with @django.db.transaction.atomic
was completed 😞
python/nav/web/api/v1/views.py
Outdated
cursor.execute( | ||
""" | ||
SELECT mac, vendor FROM temp_macaddrs INNER JOIN oui ON trunc(mac)=oui | ||
""" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A potential different way of approaching this is to avoid any use of a temporary table, and instead materialize an in-query/in-memory table to join with:
SELECT mac, vendor
FROM (
VALUES
('aa:bb:cc:dd:ee:ff'::macaddr),
('11:22:33:44:55:66'::macaddr),
('77:88:99:aa:bb:cc'::macaddr)
) AS temp_macaddrs(mac)
INNER JOIN oui ON trunc(temp_macaddrs.mac) = oui.oui;
But this of course implies that you must validate all the incoming MAC addresses in Python before attempting to build a SQL query from them (no SQL injections, please).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I would rather let postgres deal with validation of mac addresses, but maybe theres a way to do this with parametrization so you cant sql inject.
Using one query per mac address will prob make this easier I think, but I imagine it will be quite a lot slower when feeding it with a lot of mac addresses
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coercing every incoming mac address to nav.macaddress.MacAddress
before using that as input to PostgreSQL shouldn't be too difficult...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that seems reasonable, ill give it a try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented these suggestions
response = api_client.get(ENDPOINTS[endpoint]) | ||
assert response.status_code == 400 | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A vendor-endpoint specific fixture would do, I guess :)
request.data seems to be automatically conferted to a list when sending requests via cURL (and in tests), but when you use the browseable API request.data is instead a QueryDict that containst the JSON string. This adds support for both types, but its abit messy.. I assume ModelView has something built in that handles this stuff, so maybe Django has a function that can do this for us? Perhaps a Mixin or something like that
|
Fixes #3337
Adds endpoint that accepts queries of a single MAC address with GET requests using url parameter
?mac
, and a bulk query using POST with a JSON body.Endpoint:
/api/vendor/