diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 406fa85d1dbc0e11d58187909a63dc5f6285285c..16da0a69aea2ddf8db2b92635a62c8610c7e34f7 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -27,6 +27,7 @@ from gso.services.partners import ( get_partner_by_name, ) from gso.services.subscriptions import ( + get_active_pe_router_subscriptions, get_active_router_subscriptions, get_active_subscriptions_by_field_and_value, get_subscriptions, @@ -187,6 +188,28 @@ class OpenGearImportModel(BaseModel): opengear_wan_gateway: IPv4AddressType +class IasToReInterconnectImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.products.product_types.ias_to_re_interconnect.`.""" + + partner: str + ipv4_network: ipaddress.IPv4Network + ipv6_network: ipaddress.IPv6Network + router_id: str + + @classmethod + def _get_active_pe_routers(cls) -> set[str]: + return {str(router.subscription_id) for router in get_active_pe_router_subscriptions()} + + @field_validator("router_id") + def check_if_router_is_available(cls, value: str) -> str: + """Router must exist in :term:`GSO`.""" + if value not in cls._get_active_pe_routers(): + msg = f"PE Router {value} not found" + raise ValueError(msg) + + return value + + T = TypeVar( "T", SiteImportModel, @@ -195,6 +218,7 @@ T = TypeVar( SuperPopSwitchImportModel, OfficeRouterImportModel, OpenGearImportModel, + IasToReInterconnectImportModel, ) common_filepath_option = typer.Option( @@ -323,6 +347,45 @@ def import_opengear(filepath: str = common_filepath_option) -> None: ) +@app.command() +def import_ias_to_re_interconnect(filepath: str = common_filepath_option) -> None: + """Import IasToReInterconnect into GSO.""" + successfully_imported_data = [] + data = _read_data(Path(filepath)) + for details in data: + details["partner"] = "GEANT" + key = f"{details["router_id"]}-{details["ipv4_network"]}-{details["ipv6_network"]}" + typer.echo(f"Creating imported IAS to R&E Interconnect: {key}") + try: + initial_data = IasToReInterconnectImportModel(**details) + start_process("create_imported_ias_to_re_interconnect", [initial_data.model_dump()]) + successfully_imported_data.append(key) + typer.echo( + f"Successfully created {key}", + ) + except ValidationError as e: + typer.echo(f"Validation error: {e}") + + typer.echo("Waiting for the dust to settle before moving on the importing new products...") + time.sleep(1) + + # Migrate new products from imported to "full" counterpart. + imported_products = get_subscriptions( + [ProductType.IMPORTED_IAS_TO_RE_INTERCONNECT], + lifecycles=[SubscriptionLifecycle.ACTIVE], + includes=["subscription_id"], + ) + for subscription_id in imported_products: + typer.echo(f"Importing {subscription_id}") + start_process("import_ias_to_re_interconnect", [subscription_id]) + + if successfully_imported_data: + typer.echo("Successfully created imported IasToReInterconnects:") + for item in successfully_imported_data: + typer.echo(f"- {item}") + typer.echo("Please validate no more imported IasToReInterconnect products exist anymore in the database.") + + @app.command() def import_iptrunks(filepath: str = common_filepath_option) -> None: """Import IP trunks into GSO.""" diff --git a/gso/migrations/versions/2024-08-07_0f7f23d13632_add_ias_to_r_e_interconnect_and_its_.py b/gso/migrations/versions/2024-08-07_0f7f23d13632_add_ias_to_r_e_interconnect_and_its_.py new file mode 100644 index 0000000000000000000000000000000000000000..aacbc562165ed244c5a7ed2b2929d7a107e9e72f --- /dev/null +++ b/gso/migrations/versions/2024-08-07_0f7f23d13632_add_ias_to_r_e_interconnect_and_its_.py @@ -0,0 +1,83 @@ +"""Add IAS to R&E Interconnect and its imported products. + +Revision ID: 0f7f23d13632 +Revises: 41fd1ae225aq +Create Date: 2024-08-07 19:01:47.700755 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '0f7f23d13632' +down_revision = '41fd1ae225aq' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('IAS to R&E Interconnect', 'IAS to R&E Interconnect', 'IasToReInterconnect', 'IAS_TO_RE', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported IAS to R&E Interconnect', 'Imported IAS to R&E Interconnect', 'ImportedIasToReInterconnect', 'IAS_TO_RE_IMP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('IasToReInterconnectBlock', 'IAS to R&E Interconnect', 'IAS_TO_RE', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('IAS to R&E Interconnect')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock'))) + """)) + 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 ('IasToReInterconnectBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_network'))) + """)) + 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 ('IasToReInterconnectBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_network'))) + """)) + + +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 ('IasToReInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_network')) + """)) + 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 ('IasToReInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_network')) + """)) + 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 ('IasToReInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_network')) + """)) + 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 ('IasToReInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_network')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('IasToReInterconnectBlock') + """)) + conn.execute(sa.text(""" +DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect')))) + """)) + conn.execute(sa.text(""" +DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Imported IAS to R&E Interconnect', 'IAS to R&E Interconnect') + """)) diff --git a/gso/migrations/versions/2024-08-07_ba1d21f0281a_add_ias_to_r_e_interconnect_and_its_.py b/gso/migrations/versions/2024-08-07_ba1d21f0281a_add_ias_to_r_e_interconnect_and_its_.py new file mode 100644 index 0000000000000000000000000000000000000000..63cfc6e6e6891d09667cd901204609991ff800c2 --- /dev/null +++ b/gso/migrations/versions/2024-08-07_ba1d21f0281a_add_ias_to_r_e_interconnect_and_its_.py @@ -0,0 +1,45 @@ +"""Add IAS to R&E Interconnect and its Imported workflows. + +Revision ID: ba1d21f0281a +Revises: 0f7f23d13632 +Create Date: 2024-08-07 19:03:59.636996 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'ba1d21f0281a' +down_revision = '0f7f23d13632' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_ias_to_re_interconnect", + "target": "CREATE", + "description": "Create IAS to R&E Interconnect", + "product_type": "IasToReInterconnect" + }, + { + "name": "import_ias_to_re_interconnect", + "target": "MODIFY", + "description": "Import IAS to R&E Interconnect", + "product_type": "ImportedIasToReInterconnect" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 069672ffda3c401e850b7d146d37259898ebbf46..6bba69c0031da1b2784a49643296d7bb74fa3eec 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -51,6 +51,13 @@ "domain_name": ".geantip", "dns_view": "default", "network_view": "default" + }, + "IAS_TO_RE": { + "V4": {"containers": ["1.1.2.0/24"], "networks": [], "mask": 31}, + "V6": {"containers": ["dead:beef::/64"], "networks": [], "mask": 126}, + "domain_name": ".geantip", + "dns_view": "default", + "network_view": "default" } }, "MONITORING": { diff --git a/gso/products/__init__.py b/gso/products/__init__.py index 9278fbe752d1fc4614f89c3f60c72ef3021908b7..482cb00d47317f9ab1513ddf4dad952277e3aaf0 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -8,6 +8,7 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY from pydantic_forms.types import strEnum +from gso.products.product_types.ias_to_re_interconnect import IasToReInterconnect, ImportedIasToReInterconnect from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter @@ -37,6 +38,8 @@ class ProductName(strEnum): IMPORTED_OFFICE_ROUTER = "Imported office router" OPENGEAR = "Opengear" IMPORTED_OPENGEAR = "Imported Opengear" + IAS_TO_RE_INTERCONNECT = "IAS to R&E Interconnect" + IMPORTED_IAS_TO_RE_INTERCONNECT = "Imported IAS to R&E Interconnect" class ProductType(strEnum): @@ -57,6 +60,8 @@ class ProductType(strEnum): IMPORTED_OFFICE_ROUTER = ImportedOfficeRouter.__name__ OPENGEAR = Opengear.__name__ IMPORTED_OPENGEAR = Opengear.__name__ + IAS_TO_RE_INTERCONNECT = IasToReInterconnect.__name__ + IMPORTED_IAS_TO_RE_INTERCONNECT = ImportedIasToReInterconnect.__name__ SUBSCRIPTION_MODEL_REGISTRY.update( @@ -76,5 +81,7 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.IMPORTED_OFFICE_ROUTER.value: ImportedOfficeRouter, ProductName.OPENGEAR.value: Opengear, ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear, + ProductName.IAS_TO_RE_INTERCONNECT.value: IasToReInterconnect, + ProductName.IMPORTED_IAS_TO_RE_INTERCONNECT.value: ImportedIasToReInterconnect, }, ) diff --git a/gso/products/product_blocks/ias_to_re_interconnect.py b/gso/products/product_blocks/ias_to_re_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..5202caa20ddbd64658ec2ae26f93eff44c6f98c0 --- /dev/null +++ b/gso/products/product_blocks/ias_to_re_interconnect.py @@ -0,0 +1,41 @@ +"""IAS to R&E Interconnect product block that has all parameters of a subscription throughout its lifecycle.""" + +import ipaddress + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning + + +class IasToReInterconnectBlockInactive( + ProductBlockModel, + lifecycle=[SubscriptionLifecycle.INITIAL], + product_block_name="IasToReInterconnectBlock", +): + """An inactive IasToReInterconnect.""" + + ipv4_network: ipaddress.IPv4Network | None = None + ipv6_network: ipaddress.IPv6Network | None = None + router: RouterBlockInactive | None = None + + +class IasToReInterconnectBlockProvisioning( + IasToReInterconnectBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] +): + """An IasToReInterconnect that is being provisioned.""" + + ipv4_network: ipaddress.IPv4Network + ipv6_network: ipaddress.IPv6Network + router: RouterBlockProvisioning + + +class IasToReInterconnectBlock(IasToReInterconnectBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An IasToReInterconnect.""" + + #: The IPv4 address block assigned for the interconnect + ipv4_network: ipaddress.IPv4Network + #: The IPv6 address block assigned for the interconnect + ipv6_network: ipaddress.IPv6Network + #: The router associated with the IAS to R&E interconnect + router: RouterBlock diff --git a/gso/products/product_types/ias_to_re_interconnect.py b/gso/products/product_types/ias_to_re_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..5b87af305e223adf36674dc422028963748eea38 --- /dev/null +++ b/gso/products/product_types/ias_to_re_interconnect.py @@ -0,0 +1,42 @@ +"""The product type for IAS to R&E Interconnect.""" + +from orchestrator.domain.base import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.ias_to_re_interconnect import ( + IasToReInterconnectBlock, + IasToReInterconnectBlockInactive, + IasToReInterconnectBlockProvisioning, +) + + +class IasToReInterconnectInactive(SubscriptionModel, is_base=True): + """An IAS to R&E Interconnect that is inactive.""" + + ias_to_re_interconnect: IasToReInterconnectBlockInactive + + +class IasToReInterconnectProvisioning(IasToReInterconnectInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An IAS to R&E Interconnect that is being provisioned.""" + + ias_to_re_interconnect: IasToReInterconnectBlockProvisioning + + +class IasToReInterconnect(IasToReInterconnectProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An IAS to R&E Interconnect that is active.""" + + ias_to_re_interconnect: IasToReInterconnectBlock + + +class ImportedIasToReInterconnectInactive(SubscriptionModel, is_base=True): + """An imported IAS to R&E Interconnect that is inactive.""" + + ias_to_re_interconnect: IasToReInterconnectBlockInactive + + +class ImportedIasToReInterconnect( + ImportedIasToReInterconnectInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported IAS to R&E Interconnect that is active.""" + + ias_to_re_interconnect: IasToReInterconnectBlock diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index d82c66c9b0a42aa83b62fe731e40710a41c85005..91ea008bc4290bff21aa012eeb577f4fd0f557cb 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -23,7 +23,8 @@ from orchestrator.types import SubscriptionLifecycle from orchestrator.workflow import ProcessStatus from pydantic_forms.types import UUIDstr -from gso.products import ProductName, ProductType +from gso.products import ProductName, ProductType, Router +from gso.products.product_blocks.router import RouterRole from gso.products.product_types.site import Site SubscriptionType = dict[str, Any] @@ -113,6 +114,21 @@ def get_active_router_subscriptions(includes: list[str] | None = None) -> list[S ) +def get_active_pe_router_subscriptions() -> list[Router]: + """Retrieve active subscriptions specifically for PE routers. + + :return: A list of PE routers. + :rtype: list[Router] + """ + routers = get_subscriptions( + product_types=[ProductType.ROUTER], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=["subscription_id"] + ) + + subscriptions = [Router.from_subscription(r["subscription_id"]) for r in routers] + + return [router for router in subscriptions if router.router.router_role == RouterRole.PE] + + def get_provisioning_router_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: """Retrieve provisioning subscriptions specifically for routers. diff --git a/gso/settings.py b/gso/settings.py index 6c6378dd196915b26626a80ac31f8282a795791d..eb3c21730a36a0de6829b1986b6b5d4306f4d552 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -103,6 +103,7 @@ class IPAMParams(BaseSettings): GEANT_IP: ServiceNetworkParams SI: ServiceNetworkParams LT_IAS: ServiceNetworkParams + IAS_TO_RE: ServiceNetworkParams class MonitoringSNMPV2Params(BaseSettings): diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index c74df450a1c5b332b7dbc02cb96742dbb92d5901..172067180353262b0be5790893b6ac84f303aa86 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -67,6 +67,8 @@ "import_opengear": "NOT FOR HUMANS -- Finalize import into an OpenGear", "validate_iptrunk": "Validate IP Trunk configuration", "validate_router": "Validate router configuration", + "create_ias_to_re_interconnect": "Create IAS to R&E Interconnect", + "import_ias_to_re_interconnect": "NOT FOR HUMANS -- Finalize import into an IAS to R&E Interconnect product", "task_validate_geant_products": "Validation task for GEANT products", "task_send_email_notifications": "Send email notifications for failed tasks", "task_create_partners": "Create partner task", diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 56561838557882cac784faed6745c1d80661a0cd..48778b456f9be972555e1afafbd297f7f3871067 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -66,6 +66,14 @@ LazyWorkflowInstance("gso.workflows.office_router.create_imported_office_router" LazyWorkflowInstance("gso.workflows.opengear.create_imported_opengear", "create_imported_opengear") LazyWorkflowInstance("gso.workflows.opengear.import_opengear", "import_opengear") +# IAS to R&E Interconnect workflows +LazyWorkflowInstance( + "gso.workflows.ias_to_re_interconnect.create_ias_to_re_interconnect", "create_ias_to_re_interconnect" +) +LazyWorkflowInstance( + "gso.workflows.ias_to_re_interconnect.import_ias_to_re_interconnect", "import_ias_to_re_interconnect" +) + # Tasks LazyWorkflowInstance("gso.workflows.tasks.send_email_notifications", "task_send_email_notifications") LazyWorkflowInstance("gso.workflows.tasks.validate_geant_products", "task_validate_geant_products") diff --git a/gso/workflows/ias_to_re_interconnect/__init__.py b/gso/workflows/ias_to_re_interconnect/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c0b1b9e6a5b084af1b799a41a0f3455966dcb4e0 --- /dev/null +++ b/gso/workflows/ias_to_re_interconnect/__init__.py @@ -0,0 +1 @@ +"""All workflows that can be executed on IAS to R&E Interconnects.""" diff --git a/gso/workflows/ias_to_re_interconnect/create_ias_to_re_interconnect.py b/gso/workflows/ias_to_re_interconnect/create_ias_to_re_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..b3c59ac19bc3d8d61df3eb3366adbf853c813ae3 --- /dev/null +++ b/gso/workflows/ias_to_re_interconnect/create_ias_to_re_interconnect.py @@ -0,0 +1,172 @@ +"""A creation workflow that deploys a new IAS to R&E Interconnect service.""" + +import json + +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Choice +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr +from orchestrator.utils.json import json_dumps +from orchestrator.workflow import StepList, begin, done, step, workflow +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from orchestrator.workflows.utils import wrap_create_initial_input_form +from pydantic import ConfigDict, field_validator +from pydantic_forms.validators import ReadOnlyField + +from gso.products.product_types.ias_to_re_interconnect import IasToReInterconnectInactive +from gso.products.product_types.router import Router +from gso.services import infoblox, subscriptions +from gso.services.lso_client import execute_playbook, lso_interaction +from gso.services.partners import get_partner_by_name +from gso.utils.helpers import ( + validate_tt_number, +) + + +def initial_input_form_generator(product_name: str) -> FormGenerator: + """Gather input from the user in three steps.""" + routers = {} + for router in subscriptions.get_active_pe_router_subscriptions(): + routers[str(router.subscription_id)] = router.description + + router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items(), strict=True)) # type: ignore[arg-type] + + class CreateIasToReInterconnectForm(FormPage): + model_config = ConfigDict(title=product_name) + + tt_number: str + partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] + router_id: router_enum_a # type: ignore[valid-type] + + @field_validator("tt_number") + def validate_tt_number(cls, tt_number: str) -> str: + return validate_tt_number(tt_number) + + initial_user_input = yield CreateIasToReInterconnectForm + + return initial_user_input.model_dump() + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = IasToReInterconnectInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +@step("Get information from IPAM") +def get_info_from_ipam(subscription: IasToReInterconnectInactive) -> State: + """Allocate IP resources in :term:`IPAM`.""" + subscription.ias_to_re_interconnect.ipv4_network = infoblox.allocate_v4_network( + "IAS_TO_RE", # TODO ask Simone for comment + comment=f"IAS to R&E on {subscription.ias_to_re_interconnect.router.router_fqdn}", + ) + subscription.ias_to_re_interconnect.ipv6_network = infoblox.allocate_v6_network( + "IAS_TO_RE", # TODO ask Simone for comment + comment=f"IAS to R&E on {subscription.ias_to_re_interconnect.router.router_fqdn}", + ) + + return {"subscription": subscription} + + +def _provision_ias_to_re_interconnect( + callback_route: str, + process_id: UUIDstr, + subscription: IasToReInterconnectInactive, + tt_number: str, + *, + dry_run: bool, +) -> None: + extra_vars = { + "subscription": json.loads(json_dumps(subscription)), + "dry_run": dry_run, + "verb": "deploy", + "commit_comment": ( + f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy config for IAS to R&E Interconnect" + ), + } + execute_playbook( + playbook_name="ias_to_re_interconnects.yaml", + callback_route=callback_route, + inventory=f"{subscription.ias_to_re_interconnect.router.router_fqdn}\n", + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Provision IAS to R&E Interconnect") +def provision_ias_to_re_interconnect_dry( + subscription: IasToReInterconnectInactive, + callback_route: str, + process_id: UUIDstr, + tt_number: str, +) -> State: + """Perform a dry run of deploying configuration of the IAS to R&E Interconnect.""" + _provision_ias_to_re_interconnect(callback_route, process_id, subscription, tt_number, dry_run=True) + + return {"subscription": subscription} + + +@step("[FOR REAL] Provision IAS to R&E Interconnect interface") +def provision_ias_to_re_interconnect_real( + subscription: IasToReInterconnectInactive, + callback_route: str, + process_id: UUIDstr, + tt_number: str, +) -> State: + """Deploy IAS to R&E Interconnect configuration.""" + _provision_ias_to_re_interconnect(callback_route, process_id, subscription, tt_number, dry_run=False) + + return {"subscription": subscription} + + +@step("Register DNS records for both sides of the trunk") +def register_dns_records(subscription: IasToReInterconnectInactive) -> State: + """Register :term:`DNS` records for the newly created IasToReInterconnect.""" + # TODO Simone will tell me about the format + + return {"subscription": subscription} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: IasToReInterconnectInactive, + router_id: UUIDstr, +) -> State: + """Take all input from the user, and store it in the database.""" + router = Router.from_subscription(router_id).router + subscription.ias_to_re_interconnect.router = router + subscription.description = f"IAS to R&E Interconnect in: {router.router_fqdn}" + + return {"subscription": subscription} + + +@workflow( + "Create IAS to R&E Interconnect", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_ias_to_re_interconnect() -> StepList: + """Create a new IAS to R&E Interconnect. + + * Create the subscription object in the database + * Gather relevant information from Infoblox + * Deploy configuration, first as a dry run + * Set the subscription to active in the database + """ + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> get_info_from_ipam + >> lso_interaction(provision_ias_to_re_interconnect_dry) + >> lso_interaction(provision_ias_to_re_interconnect_real) + >> register_dns_records + >> set_status(SubscriptionLifecycle.PROVISIONING) + >> resync + >> done + ) diff --git a/gso/workflows/ias_to_re_interconnect/import_ias_to_re_interconnect.py b/gso/workflows/ias_to_re_interconnect/import_ias_to_re_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..1a16d9314f21a68fbeca1302c8d8f72b91788717 --- /dev/null +++ b/gso/workflows/ias_to_re_interconnect/import_ias_to_re_interconnect.py @@ -0,0 +1,35 @@ +"""A modification workflow for migrating an IasToReInterconnect to an IAS to R&E Interconnect subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.workflow import StepList, done, init, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import IasToReInterconnect, ImportedIasToReInterconnect, ProductName +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create new IAS to R&E Interconnect subscription") +def import_ias_to_re_interconnect_subscription(subscription_id: UUIDstr) -> State: + """Take an IasToReInterconnect subscription, and turn it into an IAS to R&E Interconnect subscription.""" + old_ias_to_re_interconnect = ImportedIasToReInterconnect.from_subscription(subscription_id) + new_subscription_id = get_product_id_by_name(ProductName.IAS_TO_RE_INTERCONNECT) + new_subscription = IasToReInterconnect.from_other_product(old_ias_to_re_interconnect, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow( + "Import IAS to R&E Interconnect", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None) +) +def import_ias_to_re_interconnect() -> StepList: + """Modify an ImportedIasToReInterconnect subscription into an IasToReInterconnect subscription.""" + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> import_ias_to_re_interconnect_subscription + >> resync + >> done + ) diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 95703094cf67299b6b235e914de4959ff5426832..0983154967f29540b26cbe15929c7d01c5967a52 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -48,9 +48,12 @@ from gso.utils.workflow_steps import prompt_sharepoint_checklist_url def initial_input_form_generator(product_name: str) -> FormGenerator: """Gather input from the user in three steps. General information, and information on both sides of the trunk.""" routers = {} - for router in subscriptions.get_active_router_subscriptions( + active_routers = subscriptions.get_active_router_subscriptions(includes=["subscription_id", "description"]) + provisioning_routers = subscriptions.get_provisioning_router_subscriptions( includes=["subscription_id", "description"] - ) + subscriptions.get_provisioning_router_subscriptions(includes=["subscription_id", "description"]): + ) + + for router in active_routers + provisioning_routers: # Add both provisioning and active routers, since trunks are required for promoting a router to active. routers[str(router["subscription_id"])] = router["description"]