Skip to content
Snippets Groups Projects
Commit da42a2f8 authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Add API endpoint to search hosts

JIRA INFRA-931 #action In Progress
parent bf14cc97
No related branches found
No related tags found
No related merge requests found
......@@ -203,6 +203,18 @@ def get_hosts():
return utils.get_generic_model(models.Host, order_by=models.Host.name)
@bp.route("/hosts/search")
@login_required
def search_hosts():
"""Search hosts
.. :quickref: Network; Search hosts
:query q: the search query
"""
return utils.search_generic_model(models.Host)
@bp.route("/hosts", methods=["POST"])
@login_groups_accepted("admin", "network")
def create_host():
......
......@@ -12,6 +12,7 @@ This module implements useful functions for the API.
import urllib.parse
import sqlalchemy as sa
from flask import current_app, jsonify, request
from flask_sqlalchemy import Pagination
from ..extensions import db
from .. import utils
......@@ -86,6 +87,26 @@ def get_generic_model(model, order_by=None, query=None):
return jsonify(data), 200, header
def search_generic_model(model):
"""Return filtered data from model as json
:param model: model class
:returns: filtered data from model as json
"""
kwargs = request.args.to_dict()
page = int(kwargs.pop("page", 1))
per_page = int(kwargs.pop("per_page", 20))
search = kwargs.get("q", "*")
instances, nb_filtered = model.search(search, page=page, per_page=per_page)
current_app.logger.debug(
f'Found {nb_filtered} {model.__tablename__}(s) when searching "{search}"'
)
data = [instance.to_dict(recursive=True) for instance in instances]
pagination = Pagination(None, page, per_page, nb_filtered, None)
header = build_pagination_header(pagination, request.base_url, **kwargs)
return jsonify(data), 200, header
def create_generic_model(model, mandatory_fields=("name",), **kwargs):
data = request.get_json()
if data is None:
......
......@@ -137,6 +137,77 @@ If you want to retrieve all items with the status "Loaned".
...
]
Searching
---------
To search hosts, you can use a :http:get:`/api/v1/network/hosts/search` with your query
passed using the ``q`` query parameter.
- To search all hosts where the string "archiver" appears in any field, use ``q=archiver``::
curl -v -H "Authorization: Bearer $TOKEN" $URL/api/v1/network/hosts/search?q=archiver
Example response::
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 6336
< X-Total-Count: 5
< Set-Cookie: session=9afbc88a-5177-4ece-bc55-7840a47290a1; Expires=Mon, 29-Apr-2019 15:27:37 GMT; HttpOnly; Path=/
< Server: Werkzeug/0.14.1 Python/3.7.1
< Date: Fri, 29 Mar 2019 15:27:37 GMT
<
[
{
"ansible_groups": [
"aa_cluster_test",
"archiver_appliance"
],
"created_at": "2018-11-19 10:08",
"description": "test archiver",
"device_type": "VirtualMachine",
"fqdn": "archiver-04.tn.esss.lu.se",
"id": 401,
...
"name": "archiver-04",
"updated_at": "2018-11-20 07:43",
"user": "benjaminbertrand"
},
{
...
"name": "archiver-01",
"updated_at": "2018-11-28 09:29",
"user": "benjaminbertrand"
...
},
...
]
- To restrict the search to only the *name* field, use ``q=name:archiver``::
curl -v -H "Authorization: Bearer $TOKEN" $URL/api/v1/network/hosts/search?q=name:archiver
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 5374
< X-Total-Count: 4
< Set-Cookie: session=1a637fca-c94b-4ab9-b290-10d18f35453e; Expires=Mon, 29-Apr-2019 15:37:37 GMT; HttpOnly; Path=/
< Server: Werkzeug/0.14.1 Python/3.7.1
< Date: Fri, 29 Mar 2019 15:37:37 GMT
<
...
The query string is passed directly to Elasticsearch. You can use exactly the same "mini-language" as when searching
from the web user interface. Check the elasticsearch `query string syntax`_ for more details as well as the network
:ref:`network-search` for the list of fields you can use.
Note that if you pass ``q=name:arch``, this will not match *archiver* as elasticsearch uses keywords for string matching.
In this case you'd need to use a wildcard: ``q=name:arch*``. Be aware that wildcard queries can use an enormous amount
of memory and perform very badly.
Resources
---------
......@@ -175,3 +246,5 @@ Client
A Python client library is available to access CSEntry API:
`csentry-api <http://ics-infrastructure.pages.esss.lu.se/csentry-api/index.html>`_
.. _query string syntax: https://www.elastic.co/guide/en/elasticsearch/reference/6.4/query-dsl-query-string-query.html#query-string-syntax
......@@ -28,6 +28,8 @@ Stack members should be defined by linking an item to an host from the inventory
.. image:: _static/edit_item_stack_member.png
.. _network-search:
Search
------
......
......@@ -1673,3 +1673,30 @@ def test_create_cname(client, interface, admin_token):
check_response_message(
response, f"Duplicate cname on the {interface.network.domain} domain", 422
)
def test_search_hosts(client, host_factory, readonly_token):
# Create some hosts
host1 = host_factory(name="test-beautiful", description="The Zen of Python")
host_factory(name="test-explicit", description="Beautiful is better than ugly.")
host_factory(name="another-host")
# When no query is passed, all hosts are returned
response = get(client, f"{API_URL}/network/hosts/search", token=readonly_token)
assert response.status_code == 200
assert len(response.get_json()) == 3
# a keyword is searched in all fields by default
response = get(
client, f"{API_URL}/network/hosts/search?q=beautiful", token=readonly_token
)
assert response.status_code == 200
assert len(response.get_json()) == 2
# a search can be restricted to a specific field
response = get(
client, f"{API_URL}/network/hosts/search?q=name:beautiful", token=readonly_token
)
assert response.status_code == 200
r = response.get_json()
assert len(r) == 1
assert HOST_KEYS == set(r[0].keys())
assert r[0]["name"] == host1.name
assert r[0]["description"] == host1.description
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment