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