diff --git a/data/routers.json b/data/routers.json index 7e1be59aefeef98f5d641f1f1af63c500ea5885f..9827efdb27d98eb913578b38616ef719b38ced8d 100644 --- a/data/routers.json +++ b/data/routers.json @@ -1,25 +1,24 @@ -[{ - "customer": "GÉANT", - "router_site": "AMS", - "hostname": "rt1.ams.nl.lab.office", - "ts_port": 22111, - "router_vendor": "juniper", - "router_role": "p", - "router_lo_ipv4_address": "62.40.119.2", - "router_lo_ipv6_address": "2001:798:1ab::2", - "router_lo_iso_address": "49.51e5.0001.0620.4011.9002.00", - "is_ias_connected": false -}, +[ { - "customer": "GÉANT", - "router_site": "AMS", - "hostname": "rt1.ath.gr.lab.office", - "ts_port": 22111, - "router_vendor": "juniper", - "router_role": "p", - "router_lo_ipv4_address": "62.40.119.1", - "router_lo_ipv6_address": "2001:798:1ab::1", - "router_lo_iso_address": "49.51e5.0001.0620.4011.9001.00", - "is_ias_connected": false -} + "customer": "GÉANT", + "router_site": "AMS", + "hostname": "rt1", + "ts_port": 22111, + "router_vendor": "juniper", + "router_role": "p", + "router_lo_ipv4_address": "62.40.119.2", + "router_lo_ipv6_address": "2001:798:1ab::2", + "router_lo_iso_address": "49.51e5.0001.0620.4011.9002.00" + }, + { + "customer": "GÉANT", + "router_site": "AMS2", + "hostname": "rt2", + "ts_port": 22111, + "router_vendor": "juniper", + "router_role": "p", + "router_lo_ipv4_address": "62.40.119.1", + "router_lo_ipv6_address": "2001:798:1ab::1", + "router_lo_iso_address": "49.51e5.0001.0620.4011.9001.00" + } ] diff --git a/data/sites.json b/data/sites.json index 071e7984e2997165c58042f05551c554f5c70ffa..87bfd5c7c7c16999ee97eddea845801dd2e9f806 100644 --- a/data/sites.json +++ b/data/sites.json @@ -1,16 +1,15 @@ [ -{ - "site_name": "AMS2", - "site_city": "Amsterdam", - "site_country": "The Netherlands", - "site_country_code": "NL", - "site_latitude": 0, - "site_longitude": 0, - "site_bgp_community_id": 4, - "site_internal_id": 4, - "site_tier": "1", - "site_ts_address": "0.1.1.1", - "customer": "GÉANT" -} -] -//Missing: City, Country Code, Tier \ No newline at end of file + { + "site_name": "AMS2", + "site_city": "Amsterdam", + "site_country": "The Netherlands", + "site_country_code": "NL", + "site_latitude": 0, + "site_longitude": 0, + "site_bgp_community_id": 4, + "site_internal_id": 4, + "site_tier": "1", + "site_ts_address": "0.1.1.1", + "customer": "GÉANT" + } +] \ No newline at end of file diff --git a/data/sites.yaml b/data/sites.yaml deleted file mode 100644 index d10d8b6d8d4034b396fc92cf1c9272cfb1014d18..0000000000000000000000000000000000000000 --- a/data/sites.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- site_name: AMS2 - site_city: Amsterdam - site_country: The Netherlands - site_country_code: NL - site_latitude: 0 - site_longitude: 0 - site_bgp_community_id: 4 - site_internal_id: 4 - site_tier: '1' - site_ts_address: 0.1.1.1 - customer: GÉANT \ No newline at end of file diff --git a/data/trunks.json b/data/trunks.json index 1acc4b7198f21a4b0be775173fbc5c77f45f311f..38fd0d9e8525a1beeb80ab90e8954c83249f377d 100644 --- a/data/trunks.json +++ b/data/trunks.json @@ -1,28 +1,30 @@ -[{ - "customer": "GÉANT", - "geant_s_sid": "12", - "iptrunk_type": "Dark_fiber", - "iptrunk_description": "Description", - "iptrunk_speed": "100G", - "iptrunk_minimum_links": 1, - "side_a_node_id": "", - "side_a_ae_iface": "string", - "side_a_ae_geant_a_sid": "string", - "side_a_ae_members": [ - { - "interface_name": "string", - "interface_description": "string" - } - ], - "side_b_node_id": "string", - "side_b_ae_iface": "string", - "side_b_ae_geant_a_sid": "string", - "side_b_ae_members": [ - { - "interface_name": "string", - "interface_description": "string" - } - ], - "iptrunk_ipv4_network": "string", - "iptrunk_ipv6_network": "string" // calculated from ipv6 -}] +[ + { + "customer": "GÉANT", + "geant_s_sid": "12", + "iptrunk_type": "Dark_fiber", + "iptrunk_description": "Description", + "iptrunk_speed": "100G", + "iptrunk_minimum_links": 1, + "side_a_node_id": "", + "side_a_ae_iface": "string", + "side_a_ae_geant_a_sid": "string", + "side_a_ae_members": [ + { + "interface_name": "string", + "interface_description": "string" + } + ], + "side_b_node_id": "string", + "side_b_ae_iface": "string", + "side_b_ae_geant_a_sid": "string", + "side_b_ae_members": [ + { + "interface_name": "string", + "interface_description": "string" + } + ], + "iptrunk_ipv4_network": "string", + "iptrunk_ipv6_network": "string" + } +] diff --git a/data/trunks.yaml b/data/trunks.yaml index 194fb83b4669801bdf036551fe8e3b98d67246ce..f1160be24fd7941f2d7960b866a822a935395021 100644 --- a/data/trunks.yaml +++ b/data/trunks.yaml @@ -1,12 +1,12 @@ - id: LGS-00001 # Comes from SM identifies the Trunk config: common: - link_speed: 100g + link_speed: 100G minimum_links: 1 isis_metric: 500 type: "Leased" nodeA: - name: rt1.ath.gr.lab.office.geant.net + name: rt1.ams.nl.geant.net ae_name: ae0 port_sid: LGA-00001 # Comes from SM - identifies the ae members: @@ -15,7 +15,7 @@ ipv4_address: 62.40.98.0/31 ipv6_address: 2001:798:cc::1/126 nodeB: - name: rt1.ams.nl.lab.office.geant.net + name: rt2.ams2.nl.geant.net ae_name: ae0 port_sid: LGA-00002 # Comes from SM - identifies the ae members: diff --git a/gso/__init__.py b/gso/__init__.py index e400cf379de6d6a60bc3e2841e11b96d8636ce21..307827996c61b8e639428cbf9798abea341ff98b 100644 --- a/gso/__init__.py +++ b/gso/__init__.py @@ -2,13 +2,12 @@ import typer from orchestrator import OrchestratorCore, app_settings +from orchestrator.cli.main import app as cli_app # noinspection PyUnresolvedReferences import gso.products import gso.workflows # noqa: F401 from gso.api import router as api_router -from gso.cli import imports -from orchestrator.cli.main import app as cli_app def init_gso_app() -> OrchestratorCore: @@ -25,7 +24,7 @@ def init_worker_app() -> OrchestratorCore: def init_cli_app() -> typer.Typer: """Initialise :term:`GSO` as a CLI application.""" - from gso.cli import import_sites, netbox # noqa: PLC0415 + from gso.cli import imports, netbox # noqa: PLC0415 cli_app.add_typer(imports.app, name="import-cli") cli_app.add_typer(netbox.app, name="netbox-cli") diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 335d0f5ebf72257173e3661e415279eab7bfb9e3..2adfac2a391d55173ab7318dc2c6686ed8872baa 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -2,20 +2,20 @@ import ipaddress import json -import os -from typing import TypeVar, Type, Any +from pathlib import Path +from typing import TypeVar import typer -import yaml +import yaml # type: ignore[import-untyped] from pydantic import ValidationError from gso.api.v1.imports import ( - SiteImportModel, - import_site, - RouterImportModel, - import_router, IptrunkImportModel, + RouterImportModel, + SiteImportModel, import_iptrunk, + import_router, + import_site, ) from gso.services.subscriptions import get_active_subscriptions_by_field_and_value @@ -23,26 +23,37 @@ app: typer.Typer = typer.Typer() T = TypeVar("T", SiteImportModel, RouterImportModel, IptrunkImportModel) +common_filepath_option = typer.Option( + default="data.json", + help="Path to the file", +) -def read_data(filepath: str) -> dict | None: - """Read data from a JSON or YAML file.""" +def read_data(filepath: str) -> dict: + """Read data from a JSON or YAML file.""" typer.echo(f"Starting import from {filepath}") - _, file_extension = os.path.splitext(filepath) + file_path = Path(filepath) + file_extension = file_path.suffix.lower() - with open(filepath, "r") as f: - if file_extension.lower() == ".json": + with file_path.open("r") as f: + supported_extensions = {".json", ".yaml", ".yml"} + + if file_extension == ".json": return json.load(f) - elif file_extension.lower() in (".yaml", ".yml"): + if file_extension in supported_extensions: return yaml.safe_load(f) - else: - typer.echo(f"Unsupported file format: {file_extension}") - return + typer.echo(f"Unsupported file format: {file_extension}") + raise typer.Exit(code=1) -def generic_import_data(filepath: str, import_model: Type[T], import_function: callable, name_key: str): - """Generic function to import data from a JSON or YAML file.""" +def generic_import_data( + filepath: str, + import_model: type[T], + import_function: callable, # type: ignore[valid-type] + name_key: str, +) -> None: + """Import data from a JSON or YAML file.""" successfully_imported_data = [] data = read_data(filepath) for details in data: @@ -50,9 +61,11 @@ def generic_import_data(filepath: str, import_model: Type[T], import_function: c typer.echo(f"Importing {name_key}: {details[name_key]}") try: initial_data = import_model(**details) - import_function(initial_data) + import_function(initial_data) # type: ignore[misc] successfully_imported_data.append(getattr(initial_data, name_key)) - typer.echo(f"Successfully imported {name_key}: {getattr(initial_data, name_key)}") + typer.echo( + f"Successfully imported {name_key}: {getattr(initial_data, name_key)}", + ) except ValidationError as e: typer.echo(f"Validation error: {e}") @@ -63,55 +76,52 @@ def generic_import_data(filepath: str, import_model: Type[T], import_function: c @app.command() -def import_sites( - filepath: str = typer.Option(help="Path to the file containing the sites to import.", default="gso/sites.json") -): - """ - Import sites into GSO. - """ +def import_sites(filepath: str) -> None: + """Import sites into GSO.""" # Use the import_data function to handle common import logic generic_import_data(filepath, SiteImportModel, import_site, "site_name") @app.command() -def import_routers( - filepath: str = typer.Option( - help="Path to the file containing the routers to import.", default="gso/routers.json", show_default=True - ) -): - """ - Import routers into GSO. - """ +def import_routers(filepath: str) -> None: + """Import routers into GSO.""" # Use the import_data function to handle common import logic generic_import_data(filepath, RouterImportModel, import_router, "hostname") def get_router_subscription_id(node_name: str) -> str | None: """Get the subscription id for a router by its node name.""" - - subscriptions = get_active_subscriptions_by_field_and_value("router_fqdn", node_name) + subscriptions = get_active_subscriptions_by_field_and_value( + "router_fqdn", + node_name, + ) if subscriptions: return str(subscriptions[0].subscription_id) - else: - return None + return None @app.command() -def import_iptrunks( - filepath: str = typer.Option( - help="Path to the file containing the routers to import.", default="gso/trunks.json", show_default=True - ) -): - """ - Import IP trunks into GSO. - """ +def import_iptrunks(filepath: str) -> None: + """Import IP trunks into GSO.""" successfully_imported_data = [] data = read_data(filepath) for trunk in data: - ipv4_network_a = ipaddress.ip_network(trunk["config"]["nodeA"]["ipv4_address"], strict=False) - ipv4_network_b = ipaddress.ip_network(trunk["config"]["nodeB"]["ipv4_address"], strict=False) - ipv6_network_a = ipaddress.ip_network(trunk["config"]["nodeA"]["ipv6_address"], strict=False) - ipv6_network_b = ipaddress.ip_network(trunk["config"]["nodeB"]["ipv6_address"], strict=False) + ipv4_network_a = ipaddress.ip_network( + trunk["config"]["nodeA"]["ipv4_address"], + strict=False, + ) + ipv4_network_b = ipaddress.ip_network( + trunk["config"]["nodeB"]["ipv4_address"], + strict=False, + ) + ipv6_network_a = ipaddress.ip_network( + trunk["config"]["nodeA"]["ipv6_address"], + strict=False, + ) + ipv6_network_b = ipaddress.ip_network( + trunk["config"]["nodeB"]["ipv6_address"], + strict=False, + ) # Check if IPv4 networks are equal if ipv4_network_a == ipv4_network_b: iptrunk_ipv4_network = ipv4_network_a @@ -128,26 +138,33 @@ def import_iptrunks( continue typer.echo( - f'Importing IP Trunk: {get_active_subscriptions_by_field_and_value("router_fqdn", trunk["config"]["nodeA"]["name"])}' + f"Importing IP Trunk: " + f'{get_active_subscriptions_by_field_and_value("router_fqdn", trunk["config"]["nodeA"]["name"])}', ) try: initial_data = IptrunkImportModel( customer="GÉANT", geant_s_sid=trunk["id"], iptrunk_type=trunk["config"]["common"]["type"], - iptrunk_description=" ", + iptrunk_description=trunk["config"]["common"].get("description", ""), iptrunk_speed=trunk["config"]["common"]["link_speed"], iptrunk_minimum_links=trunk["config"]["common"]["minimum_links"], - side_a_node_id=get_router_subscription_id(trunk["config"]["nodeA"]["name"]), + side_a_node_id=get_router_subscription_id( + trunk["config"]["nodeA"]["name"], + ) + or "", side_a_ae_iface=trunk["config"]["nodeA"]["ae_name"], side_a_ae_geant_a_sid=trunk["config"]["nodeA"]["port_sid"], side_a_ae_members=trunk["config"]["nodeA"]["members"], - side_b_node_id=get_router_subscription_id(trunk["config"]["nodeB"]["name"]), + side_b_node_id=get_router_subscription_id( + trunk["config"]["nodeB"]["name"], + ) + or "", side_b_ae_iface=trunk["config"]["nodeB"]["ae_name"], side_b_ae_geant_a_sid=trunk["config"]["nodeB"]["port_sid"], side_b_ae_members=trunk["config"]["nodeB"]["members"], - iptrunk_ipv4_network=iptrunk_ipv4_network, - iptrunk_ipv6_network=iptrunk_ipv6_network, + iptrunk_ipv4_network=iptrunk_ipv4_network, # type:ignore[arg-type] + iptrunk_ipv6_network=iptrunk_ipv6_network, # type:ignore[arg-type] ) import_iptrunk(initial_data) successfully_imported_data.append(trunk["id"]) @@ -156,6 +173,6 @@ def import_iptrunks( typer.echo(f"Validation error: {e}") if successfully_imported_data: - typer.echo(f"Successfully imported IP Trunks:") + typer.echo("Successfully imported IP Trunks:") for item in successfully_imported_data: typer.echo(f"- {item}") diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py index b3f9e1e9c9186bab70ea972756ceff48f964c72e..d8e9996ba0821d48765ae36f50df1bb2aa541752 100644 --- a/gso/products/product_blocks/router.py +++ b/gso/products/product_blocks/router.py @@ -6,6 +6,7 @@ from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum from pydantic import ConstrainedInt +from gso import settings from gso.products.product_blocks.site import ( SiteBlock, SiteBlockInactive, @@ -58,7 +59,8 @@ class RouterBlockInactive( def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: """Generate an :term:`FQDN` from a hostname, site name, and a country code.""" - return f"{hostname}.{site_name.lower()}.{country_code.lower()}.geant.net" + oss = settings.load_oss_params() + return f"{hostname}.{site_name.lower()}.{country_code.lower()}{oss.IPAM.LO.domain_name}" class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): diff --git a/test/conftest.py b/test/conftest.py index 262ed33f562e55b3dc1c809ef37bc9e8169b3673..001484ac71332882f7e118bc4c29dde118194fe1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -107,7 +107,7 @@ def configuration_data() -> dict: "networks": ["dead:beef::/80"], "mask": 128, }, - "domain_name": ".lo", + "domain_name": ".geant.net", "dns_view": "default", }, "TRUNK": {