diff --git a/Changelog.md b/Changelog.md index a43c10d48d18746ec55c814297e676a300e9e7cf..caa750310b00321a244f49e80f2a236f51d27bc5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [1.1] - 2024-04-04 +- Fixed the AttributeError in the migrate_iptrunk workflow. +- Improved the delete device in Netbox functionality. + ## [1.0] - 2024-03-28 - PHASE 1 initial release diff --git a/docs/source/module/api/v1/index.rst b/docs/source/module/api/v1/index.rst index 743e1a2814efd7e63d07f87a8150b234eaaa909a..a40080e2019e321c1f4427fd41a0c58c564b5ba2 100644 --- a/docs/source/module/api/v1/index.rst +++ b/docs/source/module/api/v1/index.rst @@ -15,3 +15,4 @@ Submodules imports subscriptions processes + networks diff --git a/docs/source/module/api/v1/networks.rst b/docs/source/module/api/v1/networks.rst new file mode 100644 index 0000000000000000000000000000000000000000..e85dda9ead0424f293134cfba61a0429887c88bd --- /dev/null +++ b/docs/source/module/api/v1/networks.rst @@ -0,0 +1,6 @@ +``gso.api.v1.subscriptions`` +============================ + +.. automodule:: gso.api.v1.networks + :members: + :show-inheritance: diff --git a/gso/api/v1/__init__.py b/gso/api/v1/__init__.py index 983408986b827a35bf32cbc538d39eb7b6e208e1..c25422efbc6c7aafd29f0751f1104c819d051160 100644 --- a/gso/api/v1/__init__.py +++ b/gso/api/v1/__init__.py @@ -3,6 +3,7 @@ from fastapi import APIRouter from gso.api.v1.imports import router as imports_router +from gso.api.v1.network import router as network_router from gso.api.v1.processes import router as processes_router from gso.api.v1.subscriptions import router as subscriptions_router @@ -11,3 +12,4 @@ router = APIRouter() router.include_router(imports_router) router.include_router(subscriptions_router) router.include_router(processes_router) +router.include_router(network_router) diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index 688e0c05199dcc70ed3007fce7246393ee02c628..0b2b6b1624c06acb657cca57ebb03a0817972cf0 100644 --- a/gso/api/v1/imports.py +++ b/gso/api/v1/imports.py @@ -10,7 +10,7 @@ from orchestrator.services import processes from pydantic import BaseModel, root_validator, validator from gso.auth.security import opa_security_default -from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.services import subscriptions @@ -65,7 +65,7 @@ class IptrunkImportModel(BaseModel): geant_s_sid: str iptrunk_type: IptrunkType iptrunk_description: str - iptrunk_speed: PhyPortCapacity + iptrunk_speed: PhysicalPortCapacity iptrunk_minimum_links: int iptrunk_isis_metric: int side_a_node_id: str diff --git a/gso/api/v1/network.py b/gso/api/v1/network.py new file mode 100644 index 0000000000000000000000000000000000000000..c1e20368944d1d8cd90d5d5dd8758afb9d109f66 --- /dev/null +++ b/gso/api/v1/network.py @@ -0,0 +1,94 @@ +"""API endpoints for network related operations.""" + +from uuid import UUID + +from fastapi import APIRouter +from orchestrator.domain import SubscriptionModel +from orchestrator.schemas.base import OrchestratorBaseModel +from orchestrator.services.subscriptions import build_extended_domain_model +from starlette import status + +from gso.products.product_blocks.iptrunk import PhysicalPortCapacity +from gso.services.subscriptions import get_active_iptrunk_subscriptions + +router = APIRouter(prefix="/networks", tags=["Network"]) + + +class RouterBlock(OrchestratorBaseModel): + """Router block schema.""" + + subscription_instance_id: UUID + router_fqdn: str + + +class IptrunkSideBlock(OrchestratorBaseModel): + """Iptrunk side block schema.""" + + subscription_instance_id: UUID + iptrunk_side_node: RouterBlock + + +class IptrunkBlock(OrchestratorBaseModel): + """Iptrunk block schema.""" + + subscription_instance_id: UUID + iptrunk_speed: str + iptrunk_capacity: str + iptrunk_isis_metric: int + iptrunk_sides: list[IptrunkSideBlock] + + +class IptrunkSchema(OrchestratorBaseModel): + """Iptrunk schema.""" + + subscription_id: UUID + insync: bool + iptrunk: IptrunkBlock + + +class NetworkTopologyDomainModelSchema(OrchestratorBaseModel): + """Network topology domain model schema.""" + + iptrunks: list[IptrunkSchema] + + +def _calculate_iptrunk_capacity(iptrunk_sides: list, iptrunk_speed: PhysicalPortCapacity) -> str: + """Calculate the total capacity of an IP trunk.""" + int_iptrunk_speed = int(iptrunk_speed.value.replace("G", "")) + capacity = int_iptrunk_speed * len(iptrunk_sides[0]["iptrunk_side_ae_members"]) + return f"{capacity}G" + + +@router.get("/topology", status_code=status.HTTP_200_OK, response_model=NetworkTopologyDomainModelSchema) +def network_topology() -> NetworkTopologyDomainModelSchema: + """Retrieve all active or provisioning IP trunk subscriptions.""" + topology: dict = {"iptrunks": []} + active_iptrunks = get_active_iptrunk_subscriptions() + for iptrunk in active_iptrunks: + subscription = SubscriptionModel.from_subscription(iptrunk["subscription_id"]) + extended_model = build_extended_domain_model(subscription) + formatted_model = { + "subscription_id": extended_model["subscription_id"], + "insync": extended_model["insync"], + "iptrunk": { + "subscription_instance_id": extended_model["iptrunk"]["subscription_instance_id"], + "iptrunk_speed": extended_model["iptrunk"]["iptrunk_speed"], + "iptrunk_isis_metric": extended_model["iptrunk"]["iptrunk_isis_metric"], + "iptrunk_capacity": _calculate_iptrunk_capacity( + extended_model["iptrunk"]["iptrunk_sides"], extended_model["iptrunk"]["iptrunk_speed"] + ), + "iptrunk_sides": [ + { + "subscription_instance_id": side["subscription_instance_id"], + "iptrunk_side_node": { + "subscription_instance_id": side["iptrunk_side_node"]["subscription_instance_id"], + "router_fqdn": side["iptrunk_side_node"]["router_fqdn"], + }, + } + for side in extended_model["iptrunk"]["iptrunk_sides"] + ], + }, + } + topology["iptrunks"].append(IptrunkSchema(**formatted_model)) + + return NetworkTopologyDomainModelSchema(**topology) diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py index fa10288cea07580ffb7f5ab48cd5c1520f8d4a6c..901f37e787805e68307fc598a95080b0e6cdbd08 100644 --- a/gso/products/product_blocks/iptrunk.py +++ b/gso/products/product_blocks/iptrunk.py @@ -14,7 +14,7 @@ from gso.products.product_blocks.router import ( ) -class PhyPortCapacity(strEnum): +class PhysicalPortCapacity(strEnum): """Physical port capacity enumerator. An enumerator that has the different possible capacities of ports that are available to use in subscriptions. @@ -113,7 +113,7 @@ class IptrunkBlockInactive( geant_s_sid: str | None = None iptrunk_description: str | None = None iptrunk_type: IptrunkType | None = None - iptrunk_speed: PhyPortCapacity | None = None + iptrunk_speed: PhysicalPortCapacity | None = None iptrunk_minimum_links: int | None = None iptrunk_isis_metric: int | None = None iptrunk_ipv4_network: ipaddress.IPv4Network | None = None @@ -127,7 +127,7 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife geant_s_sid: str | None = None iptrunk_description: str | None = None iptrunk_type: IptrunkType | None = None - iptrunk_speed: PhyPortCapacity | None = None + iptrunk_speed: PhysicalPortCapacity | None = None iptrunk_minimum_links: int | None = None iptrunk_isis_metric: int | None = None iptrunk_ipv4_network: ipaddress.IPv4Network | None = None @@ -145,7 +145,7 @@ class IptrunkBlock(IptrunkBlockProvisioning, lifecycle=[SubscriptionLifecycle.AC #: The type of trunk, can be either dark fibre or leased capacity. iptrunk_type: IptrunkType #: The speed of the trunk, measured per interface associated with it. - iptrunk_speed: PhyPortCapacity + iptrunk_speed: PhysicalPortCapacity #: The minimum amount of links the trunk should consist of. iptrunk_minimum_links: int #: The :term:`ISIS` metric of this link diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py index 74f56982ae0ca33a3a4cb400f50c3f5e61ad44f4..212b1fb9d60ec1e26719a64b5074b74b4d03ae0f 100644 --- a/gso/services/netbox_client.py +++ b/gso/services/netbox_client.py @@ -1,5 +1,6 @@ """Contain all methods to communicate with the NetBox API endpoint. Data Center Infrastructure Main (DCIM).""" +from contextlib import suppress from uuid import UUID import pydantic @@ -199,8 +200,9 @@ class NetboxClient: return device def delete_device(self, device_name: str) -> None: - """Delete device by name.""" - self.netbox.dcim.devices.get(name=device_name).delete() + """Delete device by name if exists.""" + with suppress(AttributeError): + self.netbox.dcim.devices.get(name=device_name).delete() def attach_interface_to_lag( self, diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 378f32b0beebfc9012bab8605f01d6f3f33aa891..386fb36bf3d1b61e2d0a52252fc832d301a7a05c 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -20,7 +20,7 @@ from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlockInactive, IptrunkSideBlockInactive, IptrunkType, - PhyPortCapacity, + PhysicalPortCapacity, ) from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router @@ -60,7 +60,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: geant_s_sid: str iptrunk_description: str iptrunk_type: IptrunkType - iptrunk_speed: PhyPortCapacity + iptrunk_speed: PhysicalPortCapacity iptrunk_minimum_links: int @validator("tt_number", allow_reuse=True) @@ -210,7 +210,7 @@ def initialize_subscription( geant_s_sid: str, iptrunk_type: IptrunkType, iptrunk_description: str, - iptrunk_speed: PhyPortCapacity, + iptrunk_speed: PhysicalPortCapacity, iptrunk_minimum_links: int, side_a_node_id: str, side_a_ae_iface: str, diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index add6d5f01d6c5d74f31dc426453856f4240bf963..540505d061f834b13bfae8855b592f0c140decc7 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -72,7 +72,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: migrate_to_different_site: bool = False restore_isis_metric: bool = True - @validator("tt_number", allow_reuse=True, pre=True, always=True) + @validator("tt_number", allow_reuse=True, always=True) def validate_tt_number(cls, tt_number: str) -> str: return validate_tt_number(tt_number) @@ -162,7 +162,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: raise ValueError(msg) return new_lag_interface - @validator("new_lag_member_interfaces", allow_reuse=True, pre=True, always=True) + @validator("new_lag_member_interfaces", allow_reuse=True) def is_interface_names_valid_juniper(cls, new_lag_member_interfaces: list[LAGMember]) -> list[LAGMember]: vendor = get_router_vendor(new_router) return validate_interface_name_list(new_lag_member_interfaces, vendor) diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 329d84aa6ef77002e1cc89bf3aa623672dd16167..d3b5e60ee539660e7aa2fbd22088d20c8235e6cb 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -19,7 +19,7 @@ from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, - PhyPortCapacity, + PhysicalPortCapacity, ) from gso.products.product_types.iptrunk import Iptrunk from gso.services.lso_client import execute_playbook, lso_interaction @@ -86,7 +86,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: "Changing the PhyPortCapacity will result in the deletion of all AE members. " "You will need to add the new AE members in the next steps." # type: ignore[assignment] ) - iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed + iptrunk_speed: PhysicalPortCapacity = subscription.iptrunk.iptrunk_speed iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric) iptrunk_ipv4_network: ipaddress.IPv4Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv4_network) @@ -157,7 +157,7 @@ def modify_iptrunk_subscription( geant_s_sid: str, iptrunk_type: IptrunkType, iptrunk_description: str, - iptrunk_speed: PhyPortCapacity, + iptrunk_speed: PhysicalPortCapacity, iptrunk_minimum_links: int, side_a_ae_geant_a_sid: str, side_a_ae_members: list[dict], diff --git a/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py index c34be8ed4e8e9564b19eca4f9f56ef4c27b2f7bd..648d954f94ae57f9471826bba04402684508de81 100644 --- a/gso/workflows/tasks/import_iptrunk.py +++ b/gso/workflows/tasks/import_iptrunk.py @@ -12,7 +12,7 @@ from orchestrator.workflow import StepList, done, init, step from orchestrator.workflows.steps import resync, set_status, store_process_subscription from gso.products import ProductName -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhysicalPortCapacity from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router from gso.services import subscriptions @@ -42,7 +42,7 @@ def initial_input_form_generator() -> FormGenerator: geant_s_sid: str iptrunk_description: str iptrunk_type: IptrunkType - iptrunk_speed: PhyPortCapacity + iptrunk_speed: PhysicalPortCapacity iptrunk_minimum_links: int iptrunk_isis_metric: int @@ -83,7 +83,7 @@ def initialize_subscription( geant_s_sid: str, iptrunk_type: IptrunkType, iptrunk_description: str, - iptrunk_speed: PhyPortCapacity, + iptrunk_speed: PhysicalPortCapacity, iptrunk_minimum_links: int, iptrunk_isis_metric: int, side_a_node_id: str, diff --git a/setup.py b/setup.py index be3688d8df2ad8dda08a2f1ef611b261c1ce34ac..1dae648f7952ee073536bdbbd40e70cf7a7746a9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="1.0", + version="1.1", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", diff --git a/test/api/test_imports.py b/test/api/test_imports.py index e1be0d5a74c54b9dc475daf152b9e4ee3f90e4a0..f7b58f723eff687b92337281e14af11d5688cd8b 100644 --- a/test/api/test_imports.py +++ b/test/api/test_imports.py @@ -5,7 +5,7 @@ import pytest from orchestrator.db import SubscriptionTable from orchestrator.services import subscriptions -from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.utils.helpers import iso_from_ipv4 @@ -27,7 +27,7 @@ def iptrunk_data(nokia_router_subscription_factory, faker): "geant_s_sid": faker.geant_sid(), "iptrunk_type": IptrunkType.DARK_FIBER, "iptrunk_description": faker.sentence(), - "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND, + "iptrunk_speed": PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, "iptrunk_minimum_links": 5, "iptrunk_isis_metric": 500, "side_a_node_id": router_side_a, diff --git a/test/api/test_networks.py b/test/api/test_networks.py new file mode 100644 index 0000000000000000000000000000000000000000..03d039cc8fe13dd419cac897db116d87118dd88b --- /dev/null +++ b/test/api/test_networks.py @@ -0,0 +1,16 @@ +from orchestrator.types import SubscriptionLifecycle + +TOPOLOGY_ENDPOINT = "/api/v1/networks/topology" + + +def test_iptrunk_subscriptions_endpoint_with_valid_api_key(test_client, iptrunk_subscription_factory): + iptrunk_subscription_factory() + iptrunk_subscription_factory() + iptrunk_subscription_factory() + iptrunk_subscription_factory(status=SubscriptionLifecycle.TERMINATED) + iptrunk_subscription_factory(status=SubscriptionLifecycle.INITIAL) + + response = test_client.get(TOPOLOGY_ENDPOINT) + + assert response.status_code == 200 + assert len(response.json()["iptrunks"]) == 3 diff --git a/test/fixtures.py b/test/fixtures.py index f0c55c2190041a837594b7907063abcdce69e04a..2a7eab3dea34e4625beba4816741154db2d4f2a3 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -10,7 +10,7 @@ from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, - PhyPortCapacity, + PhysicalPortCapacity, ) from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier @@ -239,7 +239,7 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker, geant geant_s_sid=None, iptrunk_description=None, iptrunk_type=IptrunkType.DARK_FIBER, - iptrunk_speed=PhyPortCapacity.ONE_GIGABIT_PER_SECOND, + iptrunk_speed=PhysicalPortCapacity.ONE_GIGABIT_PER_SECOND, iptrunk_isis_metric=None, iptrunk_ipv4_network=None, iptrunk_ipv6_network=None, diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index 589fbabb9e9e918a6861af93a81827301a5627bb..34a79604ef532c41cf141214c88b5790b810aeef 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk, ProductName -from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity from gso.services.subscriptions import get_product_id_by_name from gso.utils.helpers import LAGMember from gso.utils.shared_enums import Vendor @@ -65,7 +65,7 @@ def input_form_wizard_data(request, juniper_router_subscription_factory, nokia_r "geant_s_sid": faker.geant_sid(), "iptrunk_type": IptrunkType.DARK_FIBER, "iptrunk_description": faker.sentence(), - "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND, + "iptrunk_speed": PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, "iptrunk_minimum_links": 2, } create_ip_trunk_side_a_router_name = {"side_a_node_id": router_side_a} diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 372933d78795bdcbc29800caa6ae0dfac6c27340..4dca28e963ef862f182749a9ce7c1b25c418c4b7 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk -from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity from gso.utils.shared_enums import Vendor from test.conftest import UseJuniperSide from test.workflows import ( @@ -55,7 +55,7 @@ def input_form_iptrunk_data( new_sid = faker.geant_sid() new_description = faker.sentence() new_type = IptrunkType.LEASED - new_speed = PhyPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND + new_speed = PhysicalPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND new_link_count = 2 new_side_a_sid = faker.geant_sid()