From 71df5da1ef4921e9ec12d2c244ec48d05377435d Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Mon, 28 Apr 2025 12:05:59 +0200
Subject: [PATCH] Add optional TTL security field to BGP session product block

---
 docs/includes/glossary.md                     |  2 +
 .../vocabularies/geant-jargon/accept.txt      |  2 +
 gso/cli/imports.py                            |  1 +
 ...dd_optional_ttl_security_to_bgp_session.py | 41 +++++++++++++++++++
 gso/products/product_blocks/bgp_session.py    |  4 ++
 gso/translations/en-GB.json                   |  2 +
 .../base_create_imported_l3_core_service.py   |  1 +
 .../base_create_l3_core_service.py            |  2 +
 .../base_modify_l3_core_service.py            |  6 +++
 gso/workflows/router/validate_router.py       |  2 +-
 test/fixtures/l3_core_service_fixtures.py     |  2 +
 .../test_modify_l3_core_service.py            |  6 ++-
 12 files changed, 68 insertions(+), 3 deletions(-)
 create mode 100644 gso/migrations/versions/2025-04-28_a3177c5f9641_add_optional_ttl_security_to_bgp_session.py

diff --git a/docs/includes/glossary.md b/docs/includes/glossary.md
index a29d24525..af5cb334a 100644
--- a/docs/includes/glossary.md
+++ b/docs/includes/glossary.md
@@ -36,6 +36,7 @@
 *[NREN]: National Research and Education Network
 *[OOB]: Out-of-band
 *[OSS]: Operational Support Systems
+*[OTRS]: Trouble Ticket system software package
 *[PoP]: Point of Presence
 *[REST]: Representational State Transfer
 *[RFC]: Request For Comments
@@ -44,6 +45,7 @@
 *[SNMP]: Simple Network Management Protocol
 *[SOT]: Source Of Truth
 *[TBA]: To be added
+*[TTL]: Time To Live
 *[UAT]: User Acceptance Testing
 *[VM]: Virtual Machine
 *[VRF]: Virtual Routing and Forwarding
diff --git a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt
index aeb0c40d8..a7e4a6f7c 100644
--- a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt
+++ b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt
@@ -55,6 +55,7 @@ OIDC
 OOB
 OPA
 (OSS|oss)
+OTRS
 PHASE 1
 Po[Pp]
 Pydantic
@@ -69,6 +70,7 @@ SOT
 SURF
 TBA
 TERMINATED?
+TTL
 TWAMP
 UAT
 UTC
diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index 6a82ddecc..fdcba0f6b 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -259,6 +259,7 @@ class L3CoreServiceImportModel(BaseModel):
         is_multi_hop: bool
         rtbh_enabled: bool  # whether Remote Triggered Blackhole is enabled
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
 
     class BFDSettingsModel(BaseModel):
         """BFD Settings model."""
diff --git a/gso/migrations/versions/2025-04-28_a3177c5f9641_add_optional_ttl_security_to_bgp_session.py b/gso/migrations/versions/2025-04-28_a3177c5f9641_add_optional_ttl_security_to_bgp_session.py
new file mode 100644
index 000000000..cbcb3558b
--- /dev/null
+++ b/gso/migrations/versions/2025-04-28_a3177c5f9641_add_optional_ttl_security_to_bgp_session.py
@@ -0,0 +1,41 @@
+"""Add optional TTL security to BGP session.
+
+Revision ID: a3177c5f9641
+Revises: fffe36624681
+Create Date: 2025-04-28 10:21:54.820219
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'a3177c5f9641'
+down_revision = 'fffe36624681'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('ttl_security', 'BGP TTL security') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ttl_security')))
+    """))
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ttl_security'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ttl_security'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ttl_security'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('ttl_security')
+    """))
diff --git a/gso/products/product_blocks/bgp_session.py b/gso/products/product_blocks/bgp_session.py
index 863a2da0a..638c06607 100644
--- a/gso/products/product_blocks/bgp_session.py
+++ b/gso/products/product_blocks/bgp_session.py
@@ -48,6 +48,7 @@ class BGPSessionInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INI
     bfd_enabled: bool = False
     ip_type: IPTypes | None = None
     prefix_limit: NonNegativeInt | None = None
+    ttl_security: NonNegativeInt | None = None
 
 
 class BGPSessionProvisioning(BGPSessionInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
@@ -65,6 +66,7 @@ class BGPSessionProvisioning(BGPSessionInactive, lifecycle=[SubscriptionLifecycl
     bfd_enabled: bool
     ip_type: IPTypes
     prefix_limit: NonNegativeInt | None
+    ttl_security: NonNegativeInt | None
 
 
 class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
@@ -83,6 +85,7 @@ class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE
         bfd_enabled: Settings for BFD.
         ip_type: The IP type of the session.
         prefix_limit: A prefix limit, if required.
+        ttl_security: A limit on time-to-live used for TTL security.
     """
 
     peer_address: IPAddress
@@ -97,3 +100,4 @@ class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE
     bfd_enabled: bool
     ip_type: IPTypes
     prefix_limit: NonNegativeInt | None
+    ttl_security: NonNegativeInt | None
diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json
index 98d936fa0..b1d6a36a4 100644
--- a/gso/translations/en-GB.json
+++ b/gso/translations/en-GB.json
@@ -58,6 +58,7 @@
             "v4_bgp_bfd_enabled": "IPv4 BGP - BFD enabled",
             "v4_bgp_multipath_enabled":  "IPv4 - BGP multipath enabled",
             "v4_bgp_prefix_limit": "IPv4 - BGP prefix limit",
+            "v4_bgp_ttl_security": "IPv4 - BGP TTL security",
             "v4_bgp_is_passive":  "IPv4 - BGP is passive",
             "v4_bgp_send_default_route": "IPv4 - BGP send default route",
             "v4_bgp_add_v4_multicast":  "IPv4 - BGP add multicast",
@@ -71,6 +72,7 @@
             "v6_bgp_bfd_enabled":  "IPv6 - BGP BFD enabled",
             "v6_bgp_multipath_enabled": "IPv6 - BGP multipath enabled",
             "v6_bgp_prefix_limit":  "IPv6 - BGP prefix limit",
+            "v6_bgp_ttl_security":  "IPv6 - BGP TTL security",
             "v6_bgp_is_passive": "IPv6 - BGP is passive",
             "v6_bgp_send_default_route":  "IPv6 - BGP send default route",
             "v6_bgp_add_v6_multicast": "IPv6 - BGP add multicast"
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 3f8bf13f3..7261633b4 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -40,6 +40,7 @@ def initial_input_form_generator() -> FormGenerator:
         is_multi_hop: bool
         rtbh_enabled: bool
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
 
     class ServiceBindingPort(BaseModel):
         edge_port: UUIDstr
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 6d8c451ea..90221ec0e 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -70,6 +70,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         bfd_enabled: bool = False
         multipath_enabled: bool = False
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
         is_passive: bool = False
         add_v4_multicast: bool = Field(default=False, exclude=True)
         send_default_route: bool = False
@@ -91,6 +92,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         bfd_enabled: bool = False
         multipath_enabled: bool = False
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
         is_passive: bool = False
         add_v6_multicast: bool = Field(default=False, exclude=True)
         send_default_route: bool = False
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index 4185ef269..e16364ad1 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -74,6 +74,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         bfd_enabled: bool = False
         multipath_enabled: bool = False
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
         is_passive: bool = False
         add_v4_multicast: bool = Field(default=False, exclude=True)
         send_default_route: bool = False
@@ -95,6 +96,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         bfd_enabled: bool = False
         multipath_enabled: bool = False
         prefix_limit: NonNegativeInt | None = None
+        ttl_security: NonNegativeInt | None = None
         is_passive: bool = False
         add_v6_multicast: bool = Field(default=False, exclude=True)
         send_default_route: bool = False
@@ -282,6 +284,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 v4_bgp_bfd_enabled: bool = Field(v4_peer.bfd_enabled, exclude=True)
                 v4_bgp_multipath_enabled: bool = Field(v4_peer.multipath_enabled, exclude=True)
                 v4_bgp_prefix_limit: NonNegativeInt | None = Field(v4_peer.prefix_limit, exclude=True)
+                v4_bgp_ttl_security: NonNegativeInt | None = Field(v4_peer.ttl_security, exclude=True)
                 v4_bgp_is_passive: bool = Field(v4_peer.is_passive, exclude=True)
                 v4_bgp_send_default_route: bool = Field(v4_peer.send_default_route, exclude=True)
                 v4_bgp_add_v4_multicast: bool = Field(bool(IPFamily.V4MULTICAST in v4_peer.families), exclude=True)
@@ -299,6 +302,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 v6_bgp_bfd_enabled: bool = Field(v6_peer.bfd_enabled, exclude=True)
                 v6_bgp_multipath_enabled: bool = Field(v6_peer.multipath_enabled, exclude=True)
                 v6_bgp_prefix_limit: NonNegativeInt | None = Field(v6_peer.prefix_limit, exclude=True)
+                v6_bgp_ttl_security: NonNegativeInt | None = Field(v6_peer.ttl_security, exclude=True)
                 v6_bgp_is_passive: bool = Field(v6_peer.is_passive, exclude=True)
                 v6_bgp_send_default_route: bool = Field(v6_peer.send_default_route, exclude=True)
                 v6_bgp_add_v6_multicast: bool = Field(bool(IPFamily.V6MULTICAST in v6_peer.families), exclude=True)
@@ -323,6 +327,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                         bfd_enabled=self.v4_bgp_bfd_enabled,
                         multipath_enabled=self.v4_bgp_multipath_enabled,
                         prefix_limit=self.v4_bgp_prefix_limit,
+                        ttl_security=self.v4_bgp_ttl_security,
                         is_passive=self.v4_bgp_is_passive,
                         send_default_route=self.v4_bgp_send_default_route,
                         add_v4_multicast=self.v4_bgp_add_v4_multicast,
@@ -348,6 +353,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                         bfd_enabled=self.v6_bgp_bfd_enabled,
                         multipath_enabled=self.v6_bgp_multipath_enabled,
                         prefix_limit=self.v6_bgp_prefix_limit,
+                        ttl_security=self.v6_bgp_ttl_security,
                         is_passive=self.v6_bgp_is_passive,
                         send_default_route=self.v6_bgp_send_default_route,
                         add_v6_multicast=self.v6_bgp_add_v6_multicast,
diff --git a/gso/workflows/router/validate_router.py b/gso/workflows/router/validate_router.py
index 02c4b5c77..38f6c4b6e 100644
--- a/gso/workflows/router/validate_router.py
+++ b/gso/workflows/router/validate_router.py
@@ -148,7 +148,7 @@ def check_kentik_entry_exists(subscription: Router) -> None:
     license on it. This is because there can be multiple, valid, non-archiving licenses for devices.
 
     Raises:
-        ProcessFailureError when a Kentik device is missing, or configured incorrectly.
+        ProcessFailureError: when a Kentik device is missing, or configured incorrectly.
     """
     client = KentikClient()
 
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index 04e9c175f..d23bc4297 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -62,6 +62,7 @@ def bgp_session_subscription_factory(faker):
         has_custom_policies: bool = False,
         multipath_enabled: bool | None = True,
         prefix_limit: NonNegativeInt | None = None,
+        ttl_security: NonNegativeInt | None = None,
         send_default_route: bool | None = True,
         is_passive: bool | None = False,
         rtbh_enabled: bool | None = False,
@@ -77,6 +78,7 @@ def bgp_session_subscription_factory(faker):
             authentication_key=authentication_key or faker.password(),
             multipath_enabled=multipath_enabled,
             prefix_limit=prefix_limit,
+            ttl_security=ttl_security,
             send_default_route=send_default_route,
             is_multi_hop=is_multi_hop,
             rtbh_enabled=rtbh_enabled,
diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index 0b2a9f3f9..93f8680b6 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -68,7 +68,8 @@ def test_modify_l3_core_service_add_new_edge_port_success(
                 "authentication_key": faker.password(),
                 "peer_address": faker.ipv4(),
                 "bfd_enabled": False,
-                "prefix_limit": 1000,
+                "prefix_limit": faker.random_int(min=500, max=1000),
+                "ttl_security": faker.random_int(max=255),
             },
             "v6_bgp_peer": {
                 "authentication_key": faker.password(),
@@ -142,7 +143,8 @@ def sbp_input_form_data(faker):
             "v6_bgp_is_passive": True,
             "v6_bgp_peer_address": faker.ipv6(),
             "v6_bgp_add_v6_multicast": True,
-            "v6_bgp_prefix_limit": 3000,
+            "v6_bgp_prefix_limit": faker.random_int(min=2500, max=3000),
+            "v6_bgp_ttl_security": faker.random_int(max=255),
         }
 
     return _generate_form_data
-- 
GitLab