From 0318858e40a0174f06525ea021b72046319ac1c7 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Wed, 10 Jul 2019 12:42:55 +0200 Subject: [PATCH] Add extra items info to hosts json model Linked items are by default returned as a list of ICS id. When recursive is True, a list of {ics_id, serial_number, stack_member} is returned instead. Items are always returned sorted by stack_member or ics_id when stack_member is null. Allow to add the serial_number and stack_member to the Ansible variables in csentry dynamic inventory. Useful for switches. JIRA INFRA-1111 #action In Progress --- app/models.py | 32 +++++++++++++++++++-- tests/functional/test_api.py | 55 +++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index e7bce66..9e7dbda 100644 --- a/app/models.py +++ b/app/models.py @@ -17,6 +17,7 @@ import urllib.parse import elasticsearch import sqlalchemy as sa from enum import Enum +from operator import itemgetter, attrgetter from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.dialects import postgresql from sqlalchemy.orm import validates @@ -1144,7 +1145,16 @@ class Host(CreatedMixin, SearchableMixin, db.Model): "device_type": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, "model": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, "description": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, - "items": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, + "items": { + "properties": { + "ics_id": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, + "serial_number": { + "type": "text", + "fields": {"keyword": {"type": "keyword"}}, + }, + "stack_member": {"type": "byte"}, + } + }, "interfaces": { "properties": { "id": {"enabled": False}, @@ -1309,7 +1319,12 @@ class Host(CreatedMixin, SearchableMixin, db.Model): "device_type": str(self.device_type), "model": self.model, "description": self.description, - "items": [str(item) for item in self.items], + "items": [ + str(item) + for item in sorted( + self.items, key=attrgetter("stack_member", "ics_id") + ) + ], "interfaces": [str(interface) for interface in self.interfaces], "ansible_vars": self.ansible_vars, "ansible_groups": [str(group) for group in self.ansible_groups], @@ -1319,6 +1334,19 @@ class Host(CreatedMixin, SearchableMixin, db.Model): # Replace the list of interface names by the full representation # so that we can index everything in elasticsearch d["interfaces"] = [interface.to_dict() for interface in self.interfaces] + # Add extra info in items + # stack_member can be None, so we have to sort on ics_id as fallback + d["items"] = sorted( + [ + { + "ics_id": item.ics_id, + "serial_number": item.serial_number, + "stack_member": item.stack_member, + } + for item in self.items + ], + key=itemgetter("stack_member", "ics_id"), + ) return d diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py index 2b0da71..5ca9c1a 100644 --- a/tests/functional/test_api.py +++ b/tests/functional/test_api.py @@ -1368,7 +1368,9 @@ def test_get_hosts_with_no_model(client, host_factory, readonly_token): assert response.get_json()[0]["model"] is None -def test_get_hosts_recursive(client, host_factory, interface_factory, readonly_token): +def test_get_hosts_recursive_interfaces( + client, host_factory, interface_factory, readonly_token +): # Create some hosts with interfaces host1 = host_factory() interface11 = interface_factory(name=host1.name, host=host1) @@ -1400,6 +1402,57 @@ def test_get_hosts_recursive(client, host_factory, interface_factory, readonly_t assert rinterface21["network"] == interface21.network.vlan_name +def test_get_hosts_recursive_items(client, item_factory, host_factory, readonly_token): + host1 = host_factory() + item11 = item_factory(ics_id="AAA001", host_id=host1.id, stack_member=1) + item12 = item_factory(ics_id="AAA002", host_id=host1.id, stack_member=0) + host2 = host_factory() + item21 = item_factory(ics_id="AAB001", host_id=host2.id) + item22 = item_factory(ics_id="AAB002", host_id=host2.id) + # Without recursive, we only get the ics_id of the items + response = get(client, f"{API_URL}/network/hosts", token=readonly_token) + assert response.status_code == 200 + assert len(response.get_json()) == 2 + rhost1, rhost2 = response.get_json() + # items are sorted by stack_member + assert rhost1["items"] == ["AAA002", "AAA001"] + # or by ics_id when stack_member is None + assert rhost2["items"] == ["AAB001", "AAB002"] + # With recursive, items are expanded + response = get( + client, f"{API_URL}/network/hosts?recursive=true", token=readonly_token + ) + assert response.status_code == 200 + assert len(response.get_json()) == 2 + rhost1, rhost2 = response.get_json() + assert len(rhost1["items"]) == 2 + # Items shall be sorted by stack_member + ritem11, ritem12 = rhost1["items"] + assert ritem11 == { + "ics_id": item12.ics_id, + "serial_number": item12.serial_number, + "stack_member": 0, + } + assert ritem12 == { + "ics_id": item11.ics_id, + "serial_number": item11.serial_number, + "stack_member": 1, + } + assert len(rhost2["items"]) == 2 + # or ics_id when no stack_member + ritem21, ritem22 = rhost2["items"] + assert ritem21 == { + "ics_id": item21.ics_id, + "serial_number": item21.serial_number, + "stack_member": None, + } + assert ritem22 == { + "ics_id": item22.ics_id, + "serial_number": item22.serial_number, + "stack_member": None, + } + + def test_create_host(client, device_type_factory, user_token): device_type = device_type_factory(name="Virtual") # check that name and device_type are mandatory -- GitLab