diff --git a/cosmo/autodesc.py b/cosmo/autodesc.py index d08801d..3195cdc 100644 --- a/cosmo/autodesc.py +++ b/cosmo/autodesc.py @@ -92,7 +92,10 @@ def toDict(self) -> CosmoOutputType | Never: ) | ( # directly attached line has priority {"line": attached_tobago_line.getName()} - if attached_tobago_line and self.suppressTenant() + # two different cases: either we have to suppress tenant + # OR tenant is not defined due to no linked service definition + if (attached_tobago_line and self.suppressTenant()) + or (attached_tobago_line and not attached_tobago_line.hasTenant()) else {} ) | ( @@ -100,7 +103,9 @@ def toDict(self) -> CosmoOutputType | Never: "line": attached_tobago_line.getName(), "tenant": attached_tobago_line.getTenantName(), } - if attached_tobago_line and not self.suppressTenant() + if attached_tobago_line + and attached_tobago_line.hasTenant() + and not self.suppressTenant() else {} ) | ( diff --git a/cosmo/netbox_types.py b/cosmo/netbox_types.py index 86f1234..8dc7def 100644 --- a/cosmo/netbox_types.py +++ b/cosmo/netbox_types.py @@ -892,29 +892,35 @@ class CosmoTobagoLine(AbstractNetboxType): def getBasePath(self): return "/plugins/tobago/lines/" - def _getCurrentLineMetadata(self): - return self["version"] + def _getCurrentLineMetadata(self) -> dict: + return self.get("version", dict()) # should always be present - def _getCurrentLine(self): - return self._getCurrentLineMetadata()["line"] + def _getCurrentLine(self) -> dict: + return self._getCurrentLineMetadata().get( + "line", dict() + ) # should always be present - def _getCurrentTenant(self): - return self._getCurrentLineMetadata()["tenant"] + def _getCurrentTenant(self) -> dict | None: + return self._getCurrentLineMetadata().get("tenant") + + def hasTenant(self) -> bool: + return bool(self._getCurrentTenant()) # False on None or empty def getLineID(self) -> str: - return str(self._getCurrentLine()["id"]) + return str(self._getCurrentLine().get("id")) def getLineNameLong(self) -> str: - return str(self._getCurrentLine()["name_long"]) + return str(self._getCurrentLine().get("name_long")) def getName(self) -> str: return self.getLineNameLong() - def getTenantName(self): - return self._getCurrentTenant()["name"] - - def getLineStatus(self): - return self._getCurrentLineMetadata()["status"] + def getTenantName(self) -> str | None: + tenant = self._getCurrentTenant() + if bool(tenant) and isinstance(tenant, dict): # type safety + return tenant.get("name") + else: + return None def getRelPath(self) -> str: return urljoin(self.getBasePath(), self.getLineID()) diff --git a/cosmo/tests/test_case_auto_descriptions.yaml b/cosmo/tests/test_case_auto_descriptions.yaml index 5deb62d..84a58c1 100644 --- a/cosmo/tests/test_case_auto_descriptions.yaml +++ b/cosmo/tests/test_case_auto_descriptions.yaml @@ -262,6 +262,94 @@ device_list: name: TEST-VLAN-3 vid: 3 vrf: null + - custom_fields: + inner_tag: null + outer_tag: null + description: null + __typename: InterfaceType + enabled: true + id: '30754' + ip_addresses: [ ] + connected_endpoints: null + attached_tobago_line: + __typename: CosmoTobagoLine + component_type: CABLE + element: + description: '' + display: cable 000128935 + id: 39646 + label: cable 000128935 + url: https://netbox.example.com/api/dcim/cables/39646/ + element_id: 93828 + element_type: dcim.cable + id: 5111 + index: 1 + termination_a: + _occupied: true + cable: + description: '' + display: cable 000128935 + id: 39646 + label: cable 000128935 + url: https://netbox.example.com/api/dcim/cables/39646/ + description: '' + device: + description: '' + display: TEST0001 + id: 1747 + name: TEST0001 + url: https://netbox.example.com/api/dcim/devices/1747/ + display: et-0/0/1.4 + id: 30750 + name: et-0/0/1.4 + url: https://netbox.example.com/api/dcim/interfaces/30754/ + termination_a_id: 92388 + termination_a_type: dcim.interface + termination_b: + _occupied: true + id: 198209 + url: https://netbox.example.com/api/dcim/interfaces/192879/ + display: xe-0/1/4 + device: + id: 39827 + url: https://netbox.example.com/api/dcim/device/39827/ + display: TEST0002 + name: TEST0002 + description: null + name: xe-0/1/4 + description: "customer A" + termination_b_id: 454 + termination_b_type: dcim.interface + version: + created: '2025-09-03T11:34:19.643456+01:00' + custom_fields: { } + id: 2617 + last_updated: '2025-09-03T11:34:40.819703+01:00' + line: + display: cl390287 + id: 9834 + name: '390287' + name_long: cl390287 + url: https://netbox.example.com/api/plugins/tobago/lines/9834/ + service: null + status: current + lag: null + mac_address: 94:BF:41:41:41:F4 + mode: ACCESS + mtu: null + name: et-0/0/1.4 + tagged_vlans: [ ] + tags: + - __typename: TagType + name: edge:customer + slug: edge_customer + type: virtual + untagged_vlan: + __typename: VLANType + id: '8237463' + name: TEST-VLAN-4 + vid: 4 + vrf: null - custom_fields: inner_tag: null outer_tag: null diff --git a/cosmo/tests/test_serializer.py b/cosmo/tests/test_serializer.py index 46ca148..d8f663a 100644 --- a/cosmo/tests/test_serializer.py +++ b/cosmo/tests/test_serializer.py @@ -230,6 +230,7 @@ def test_router_interface_auto_description(): assert { "connected_endpoints": [{"name": "combo1", "device": "mikrotik01"}] } == json.loads(sd["interfaces"]["et-0/0/0"]["description"]) + # auto type description now overrides existing descriptions # assert "do not overwrite me!" == sd["interfaces"]["et-0/0/1"]["description"] assert { @@ -238,12 +239,26 @@ def test_router_interface_auto_description(): "connected_endpoints": [{"name": "combo2", "device": "mikrotik01"}], "type": "peering", } == json.loads(sd["interfaces"]["et-0/0/1"]["units"][2]["description"]) + + # sub interface with line with service, connected endpoint and link peers + # should display line info, tenant info, connected_endpoint and type assert { "line": "cl390287", "tenant": "Contoso Ltd.", "connected_endpoints": [{"name": "combo2", "device": "mikrotik01"}], "type": "customer", } == json.loads(sd["interfaces"]["et-0/0/1"]["units"][3]["description"]) + + # sub interface with line without service should display line number but not tenant + # and/or service information + assert { + "connected_endpoints": [{"name": "combo2", "device": "mikrotik01"}], + "line": "cl390287", + "type": "customer", + } == json.loads(sd["interfaces"]["et-0/0/1"]["units"][4]["description"]) + + # interface with only link_peers and no connected_endpoints should display + # link_peer information assert { "link_peers": [{"name": "port_45", "device": "Panel48673"}], "type": "access",