diff --git a/Changelog.md b/Changelog.md index 43ee4a2cea5e839c0e626e65a53c650d31ca4e83..8b2cbccbab074be1a7047c2eebf63a554b04d895 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.9] - 2024-03-20 +- `migrate_iptrunk` workflow includes Ansible trunk checks. +- `create_iptrunk` and `migrate_iptrunk` now update IPAM / DNS. +- lso result step title is now always the name of the provisioning step ## [0.8] - 2024-02-28 - Add two new workflows for "activating" `Router` and `Iptrunk` products. diff --git a/docs/source/conf.py b/docs/source/conf.py index 74502fb22f8ea416e23b894aae62d6e1d7727368..a38860b64ffea8f386ac3711263d7e88bcf2ba63 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,6 +1,6 @@ # -- Project information ----------------------------------------------------- project = "GÉANT Service Orchestrator" -copyright = "2023, GÉANT Vereniging" +copyright = "2023-2024, GÉANT Vereniging" author = "GÉANT Orchestration and Automation Team" # -- General configuration --------------------------------------------------- diff --git a/docs/source/module/services/index.rst b/docs/source/module/services/index.rst index d74c2ba8ffee28fad531d1b74ef54c5c8edab88b..7b1a1d5540b8c52e0f64c466184864339b36723a 100644 --- a/docs/source/module/services/index.rst +++ b/docs/source/module/services/index.rst @@ -15,6 +15,6 @@ Submodules crm infoblox librenms_client + lso_client netbox_client - provisioning_proxy subscriptions diff --git a/docs/source/module/services/lso_client.rst b/docs/source/module/services/lso_client.rst new file mode 100644 index 0000000000000000000000000000000000000000..98b9eee9b4c7dcff3fc1b47cde4c3ac3ae3b40a9 --- /dev/null +++ b/docs/source/module/services/lso_client.rst @@ -0,0 +1,6 @@ +``gso.services.lso_client`` +=========================== + +.. automodule:: gso.services.lso_client + :members: + :show-inheritance: diff --git a/docs/source/module/services/provisioning_proxy.rst b/docs/source/module/services/provisioning_proxy.rst deleted file mode 100644 index 756edf2642d865e3104af928fbddd047c3d4ed32..0000000000000000000000000000000000000000 --- a/docs/source/module/services/provisioning_proxy.rst +++ /dev/null @@ -1,6 +0,0 @@ -``gso.services.provisioning_proxy`` -=================================== - -.. automodule:: gso.services.provisioning_proxy - :members: - :show-inheritance: diff --git a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt index 9172c42e39a10632a75a6e73c17dd9b0e9b5eb41..2b2d2a4f8b71a93a3dbce06687222161e1d1db01 100644 --- a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt +++ b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt @@ -6,6 +6,7 @@ WFO Ansible [Dd]eprovision API +DNS dry_run Dark_fiber [A|a]ddress diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index dd77bc80e6145ae35d75fdd9514cf564d606d98c..688e0c05199dcc70ed3007fce7246393ee02c628 100644 --- a/gso/api/v1/imports.py +++ b/gso/api/v1/imports.py @@ -14,7 +14,7 @@ from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.services import subscriptions -from gso.services.crm import CustomerNotFoundError, get_customer_by_name +from gso.services.partners import PartnerNotFoundError, get_partner_by_name from gso.utils.helpers import BaseSiteValidatorModel, LAGMember from gso.utils.shared_enums import PortNumber, Vendor @@ -41,13 +41,13 @@ class SiteImportModel(BaseSiteValidatorModel): site_internal_id: int site_tier: SiteTier site_ts_address: str - customer: str + partner: str class RouterImportModel(BaseModel): """Required fields for importing an existing :class:`gso.product.product_types.router`.""" - customer: str + partner: str router_site: str hostname: str ts_port: int @@ -61,7 +61,7 @@ class RouterImportModel(BaseModel): class IptrunkImportModel(BaseModel): """Required fields for importing an existing :class:`gso.products.product_types.iptrunk`.""" - customer: str + partner: str geant_s_sid: str iptrunk_type: IptrunkType iptrunk_description: str @@ -87,13 +87,13 @@ class IptrunkImportModel(BaseModel): for router in subscriptions.get_active_router_subscriptions(includes=["subscription_id"]) } - @validator("customer") - def check_if_customer_exists(cls, value: str) -> str: - """Validate that the customer exists.""" + @validator("partner") + def check_if_partner_exists(cls, value: str) -> str: + """Validate that the partner exists.""" try: - get_customer_by_name(value) - except CustomerNotFoundError as e: - msg = f"Customer {value} not found" + get_partner_by_name(value) + except PartnerNotFoundError as e: + msg = f"partner {value} not found" raise ValueError(msg) from e return value @@ -140,7 +140,7 @@ class IptrunkImportModel(BaseModel): class SuperPopSwitchImportModel(BaseModel): """Required fields for importing an existing :class:`gso.product.product_types.super_pop_switch`.""" - customer: str + partner: str super_pop_switch_site: str hostname: str super_pop_switch_ts_port: PortNumber @@ -150,7 +150,7 @@ class SuperPopSwitchImportModel(BaseModel): class OfficeRouterImportModel(BaseModel): """Required fields for importing an existing :class:`gso.product.product_types.office_router`.""" - customer: str + partner: str office_router_site: str office_router_fqdn: str office_router_ts_port: PortNumber diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 1a20c14be3840b76a6085f8756af943ef27c9b3d..64a6af56cdb691a15c796fc27fce8e0d297b7950 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -1,13 +1,17 @@ """:term:`CLI` command for importing data to coreDB.""" +import csv import ipaddress import json +from datetime import UTC, datetime from pathlib import Path from typing import TypeVar import typer import yaml +from orchestrator.db import db from pydantic import ValidationError +from sqlalchemy.exc import SQLAlchemyError from gso.api.v1.imports import ( IptrunkImportModel, @@ -21,6 +25,7 @@ from gso.api.v1.imports import ( import_site, import_super_pop_switch, ) +from gso.db.models import PartnerTable from gso.services.subscriptions import get_active_subscriptions_by_field_and_value app: typer.Typer = typer.Typer() @@ -63,7 +68,7 @@ def generic_import_data( successfully_imported_data = [] data = read_data(filepath) for details in data: - details["customer"] = "GÉANT" + details["partner"] = "GEANT" typer.echo(f"Importing {name_key}: {details[name_key]}") try: initial_data = import_model(**details) @@ -163,7 +168,7 @@ def import_iptrunks(filepath: str = common_filepath_option) -> None: ) try: initial_data = IptrunkImportModel( - customer="GÉANT", + partner="GEANT", geant_s_sid=trunk["id"], iptrunk_type=trunk["config"]["common"]["type"], iptrunk_description=trunk["config"]["common"].get("description", ""), @@ -197,3 +202,34 @@ def import_iptrunks(filepath: str = common_filepath_option) -> None: typer.echo("Successfully imported IP Trunks:") for item in successfully_imported_data: typer.echo(f"- {item}") + + +def import_partners_from_csv(file_path: Path) -> list[dict]: + """Read partners from a CSV file.""" + with Path.open(file_path, encoding="utf-8") as csv_file: + csv_reader = csv.DictReader(csv_file) + return list(csv_reader) + + +@app.command() +def import_partners(file_path: str = typer.Argument(..., help="Path to the CSV file containing partners")) -> None: + """Import partners from a CSV file into the database.""" + typer.echo(f"Importing partners from {file_path} ...") + + partners = import_partners_from_csv(Path(file_path)) + + try: + for partner in partners: + if partner.get("created_at"): + partner["created_at"] = datetime.strptime(partner["created_at"], "%Y-%m-%d").replace(tzinfo=UTC) + + new_partner = PartnerTable(**partner) + db.session.add(new_partner) + + db.session.commit() + typer.echo(f"Successfully imported {len(partners)} partners.") + except SQLAlchemyError as e: + db.session.rollback() + typer.echo(f"Failed to import partners: {e}") + finally: + db.session.close() diff --git a/gso/cli/netbox.py b/gso/cli/netbox.py index b4a8b1b6f3a2c387f06e93c77e6956ac6389474b..8765f1a49ecc3653be9b6bf14a730080b5974177 100644 --- a/gso/cli/netbox.py +++ b/gso/cli/netbox.py @@ -14,7 +14,7 @@ def netbox_initial_setup() -> None: """Set up NetBox for the first time. It includes: - - Creating a default site (GÉANT) + - Creating a default site (GEANT) - Creating device roles (Router) """ typer.echo("Initial setup of NetBox ...") @@ -22,7 +22,7 @@ def netbox_initial_setup() -> None: nbclient = NetboxClient() - typer.echo("Creating GÉANT site ...") + typer.echo("Creating GEANT site ...") try: nbclient.create_device_site(DEFAULT_SITE["name"], DEFAULT_SITE["slug"]) typer.echo("Site created successfully.") diff --git a/gso/db/__init__.py b/gso/db/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4e2d8c84629abee88232d2c30b57caa36a2c555a --- /dev/null +++ b/gso/db/__init__.py @@ -0,0 +1 @@ +"""Initializes the GSO database module with model definitions and utilities.""" diff --git a/gso/db/models.py b/gso/db/models.py new file mode 100644 index 0000000000000000000000000000000000000000..350aa9072198c5e4adfb903f213aa681f19aca81 --- /dev/null +++ b/gso/db/models.py @@ -0,0 +1,51 @@ +"""Database model definitions and table mappings for the GSO system.""" + +import enum + +import structlog +from orchestrator.db import UtcTimestamp +from orchestrator.db.database import BaseModel +from sqlalchemy import ( + Enum, + String, + text, +) +from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.orm import mapped_column + +logger = structlog.get_logger(__name__) + + +class PartnerType(str, enum.Enum): + """Defining different types of partners in the GSO system.""" + + NREN = "NREN" + RE_PEER = "RE_PEER" + PUBLIC_PEER = "PUBLIC_PEER" + PRIVATE_PEER = "PRIVATE_PEER" + UPSTREAM = "UPSTREAM" + GEANT = "GEANT" + + +class PartnerTable(BaseModel): + """Database table for the partners in the GSO system.""" + + __tablename__ = "partners" + + partner_id = mapped_column(String, server_default=text("uuid_generate_v4"), primary_key=True) + name = mapped_column(String, unique=True) + email = mapped_column(String, unique=True, nullable=True) + as_number = mapped_column( + String, unique=True + ) # the as_number and as_set are mutually exclusive. if you give me one I don't need the other + as_set = mapped_column(String) + route_set = mapped_column(String, nullable=True) + black_listed_as_sets = mapped_column(ARRAY(String), nullable=True) + additional_routers = mapped_column(ARRAY(String), nullable=True) + additional_bgp_speakers = mapped_column(ARRAY(String), nullable=True) + + partner_type = mapped_column(Enum(PartnerType), nullable=False) + created_at = mapped_column(UtcTimestamp, server_default=text("current_timestamp"), nullable=False) + updated_at = mapped_column( + UtcTimestamp, server_default=text("current_timestamp"), nullable=False, onupdate=text("current_timestamp") + ) diff --git a/gso/migrations/env.py b/gso/migrations/env.py index 4d84cfb15787fc357dd96857fb97b4cee13b80a8..45dc109d4786205b3359743edf3681283ca58797 100644 --- a/gso/migrations/env.py +++ b/gso/migrations/env.py @@ -5,6 +5,8 @@ from orchestrator.db.database import BaseModel from orchestrator.settings import app_settings from sqlalchemy import engine_from_config, pool, text +from gso.db.models import PartnerTable # noqa: F401 + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py b/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py index d2fa1e9929d0056b408379d97fd435178e736921..12c51d34d2aba2e4eb5d569f361e9e1c25409c43 100644 --- a/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py +++ b/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py @@ -10,9 +10,9 @@ from alembic import op # revision identifiers, used by Alembic. revision = 'e8378fbcfbf3' -down_revision = None +down_revision = 'da5c9f4cce1c' branch_labels = ('data',) -depends_on = 'da5c9f4cce1c' +depends_on = None def upgrade() -> None: diff --git a/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py b/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py index aac35ee3331ab554593c1015cd655d69869b8b3c..df3ddd20f1371e6ca287df350cf9ed40eb444c27 100644 --- a/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py +++ b/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py @@ -10,7 +10,7 @@ from alembic import op # revision identifiers, used by Alembic. revision = '1c3c90ea1d8c' -down_revision = '113a81d2a40a' +down_revision = '5bea5647f61d' branch_labels = None depends_on = None diff --git a/gso/migrations/versions/2024-03-18_eaed66b04913_added_partner_table.py b/gso/migrations/versions/2024-03-18_eaed66b04913_added_partner_table.py new file mode 100644 index 0000000000000000000000000000000000000000..57ce58d5a394be806d1036e6ae2f3f6e988a44a6 --- /dev/null +++ b/gso/migrations/versions/2024-03-18_eaed66b04913_added_partner_table.py @@ -0,0 +1,67 @@ +"""Added Partner table. + +Revision ID: eaed66b04913 +Revises: 6e4952687205 +Create Date: 2024-03-18 15:03:32.721760 + +""" +import sqlalchemy as sa +from alembic import op +from orchestrator.db import UtcTimestamp +from sqlalchemy.dialects import postgresql +# revision identifiers, used by Alembic. +revision = 'eaed66b04913' +down_revision = '6e4952687205' +branch_labels = None +depends_on = None + +def upgrade() -> None: + op.create_table('partners', +sa.Column('partner_id', sa.String(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=True), + sa.Column('as_number', sa.String(), nullable=True), + sa.Column('as_set', sa.String(), nullable=True), + sa.Column('route_set', sa.String(), nullable=True), + sa.Column('black_listed_as_sets', postgresql.ARRAY(sa.String()), nullable=True), + sa.Column('additional_routers', postgresql.ARRAY(sa.String()), nullable=True), + sa.Column('additional_bgp_speakers', postgresql.ARRAY(sa.String()), nullable=True), + sa.Column('partner_type', sa.Enum('NREN', 'RE_PEER', 'PUBLIC_PEER', 'PRIVATE_PEER', 'UPSTREAM', 'GEANT', name='partnertype'), nullable=False), + sa.Column('created_at', UtcTimestamp(timezone=True), server_default=sa.text('current_timestamp'), nullable=False), + sa.Column('updated_at', UtcTimestamp(timezone=True), server_default=sa.text('current_timestamp'), nullable=False, onupdate=sa.text('current_timestamp')), + + sa.PrimaryKeyConstraint('partner_id'), + sa.UniqueConstraint('as_number'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('name'), + ) + + # Add GEANT as a partner + conn = op.get_bind() + conn.execute( + sa.text("""INSERT INTO partners (partner_id, name, partner_type) VALUES ('8f0df561-ce9d-4d9c-89a8-7953d3ffc961', 'GEANT', 'GEANT' )""")) + + # Add foreign key constraint to existing subscriptions table + op.create_foreign_key( + 'fk_subscriptions_customer_id_partners', + 'subscriptions', + 'partners', # + ['customer_id'], + ['partner_id'] + ) + + # Make customer_id not nullable + op.alter_column('subscriptions', 'customer_id', + existing_type=sa.String(length=255), + nullable=False) + + +def downgrade() -> None: + # Drop foreign key constraint + op.drop_constraint('fk_subscriptions_customer_id_partners', 'subscriptions', type_='foreignkey') + # Make customer_id nullable again + op.alter_column('subscriptions', 'customer_id', + existing_type=sa.String(length=255), + nullable=True) + + op.drop_table('partners') \ No newline at end of file diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 2ef1cd14749760e423c59676896c32638286989b..ff6eb33dc70bcaad8cf453e5d678483cadb79c5f 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -1,6 +1,7 @@ { "GENERAL": { - "public_hostname": "https://gap.geant.org" + "public_hostname": "https://gap.geant.org", + "isis_high_metric": 999999 }, "NETBOX": { "api": "https://127.0.0.1:8000", @@ -15,37 +16,37 @@ "password": "robot-user-password" }, "LO": { - "V4": {"containers": [], "networks": ["1.1.0.0/24"], "mask": 0}, - "V6": {"containers": [], "networks": ["dead:beef::/64"], "mask": 0}, - "domain_name": ".lo", + "V4": {"containers": [], "networks": ["10.255.255.0/26"], "mask": 32}, + "V6": {"containers": [], "networks": ["dead:beef::/80"], "mask": 128}, + "domain_name": ".geant.net", "dns_view": "default", "network_view": "default" }, "TRUNK": { - "V4": {"containers": ["1.1.1.0/24"], "networks": [], "mask": 31}, - "V6": {"containers": ["dead:beef::/64"], "networks": [], "mask": 126}, + "V4": {"containers": ["10.255.255.0/24", "10.255.254.0/24"], "networks": [], "mask": 31}, + "V6": {"containers": ["dead:beef::/64", "dead:beee::/64"], "networks": [], "mask": 126}, "domain_name": ".trunk", "dns_view": "default", "network_view": "default" }, "GEANT_IP": { - "V4": {"containers": ["1.1.2.0/24"], "networks": [], "mask": 31}, - "V6": {"containers": ["dead:beef::/64"], "networks": [], "mask": 126}, + "V4": {"containers": ["10.255.255.0/24", "10.255.254.0/24"], "networks": [], "mask": 31}, + "V6": {"containers": ["dead:beef::/64", "dead:beee::/64"], "networks": [], "mask": 126}, "domain_name": ".geantip", "dns_view": "default", "network_view": "default" }, "SI": { - "V4": {"containers": ["1.1.3.0/24"], "networks": [], "mask": 31}, - "V6": {"containers": ["dead:beef::/64"], "networks": [], "mask": 126}, - "domain_name": ".si", + "V4": {"containers": ["10.255.253.128/25"], "networks": [], "mask": 31}, + "V6": {"containers": [], "networks": [], "mask": 126}, + "domain_name": ".geantip", "dns_view": "default", "network_view": "default" }, "LT_IAS": { - "V4": {"containers": ["1.1.4.0/24"], "networks": [], "mask": 31}, - "V6": {"containers": ["dead:beef::/64"], "networks": [], "mask": 126}, - "domain_name": ".ltias", + "V4": {"containers": ["10.255.255.0/24"], "networks": [], "mask": 31}, + "V6": {"containers": ["dead:beef:cc::/48"], "networks": [], "mask": 126}, + "domain_name": ".geantip", "dns_view": "default", "network_view": "default" } @@ -90,5 +91,8 @@ "starttls_enabled": true, "smtp_username": "username", "smtp_password": "password" + }, + "SHAREPOINT": { + "checklist_site_url": "https://example.sharepoint.com/sites/example-site" } } diff --git a/gso/schema/__init__.py b/gso/schema/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..20b21e2c5736e9d2890482561fd61bc06a84e3c9 --- /dev/null +++ b/gso/schema/__init__.py @@ -0,0 +1 @@ +"""It is used to group the schema files together as a package.""" diff --git a/gso/schema/partner.py b/gso/schema/partner.py new file mode 100644 index 0000000000000000000000000000000000000000..890adcb9b20b08f6c244e8986ad20eaf4def83fc --- /dev/null +++ b/gso/schema/partner.py @@ -0,0 +1,30 @@ +"""Partner schema module.""" + +from datetime import datetime +from uuid import uuid4 + +from pydantic import BaseModel, EmailStr, Field + +from gso.db.models import PartnerType + + +class PartnerCreate(BaseModel): + """Partner create schema.""" + + partner_id: str = Field(default_factory=lambda: str(uuid4())) + name: str + email: EmailStr | None = None + as_number: str | None = Field(None, unique=True) + as_set: str | None = None + route_set: str | None = None + black_listed_as_sets: list[str] | None = None + additional_routers: list[str] | None = None + additional_bgp_speakers: list[str] | None = None + partner_type: PartnerType + created_at: datetime = Field(default_factory=lambda: datetime.now().astimezone()) + updated_at: datetime = Field(default_factory=lambda: datetime.now().astimezone()) + + class Config: + """Pydantic model configuration.""" + + orm_mode = True diff --git a/gso/services/crm.py b/gso/services/crm.py deleted file mode 100644 index 810df588e5b3c93081be1796d78bf9b21f4ae23f..0000000000000000000000000000000000000000 --- a/gso/services/crm.py +++ /dev/null @@ -1,31 +0,0 @@ -"""A module that returns the customers available in :term:`GSO`. - -For the time being, it's hardcoded to only contain GÉANT as a customer, since this is needed for the deployment of phase -1. -""" - -from typing import Any - - -class CustomerNotFoundError(Exception): - """Exception raised when a customer is not found.""" - - -def all_customers() -> list[dict]: - """Hardcoded list of customers available in :term:`GSO`.""" - return [ - { - "id": "8f0df561-ce9d-4d9c-89a8-7953d3ffc961", - "name": "GÉANT", - }, - ] - - -def get_customer_by_name(name: str) -> dict[str, Any]: - """Try to get a customer by their name.""" - for customer in all_customers(): - if customer["name"] == name: - return customer - - msg = f"Customer {name} not found" - raise CustomerNotFoundError(msg) diff --git a/gso/services/infoblox.py b/gso/services/infoblox.py index 8c23116a7ef8d4d3124cb89cb212e2a13dcfb9fc..ca01bcabb8727a92aa0a2912bcf142a2a550c3cd 100644 --- a/gso/services/infoblox.py +++ b/gso/services/infoblox.py @@ -257,8 +257,8 @@ def create_host_by_ip( raise AllocationError(msg) conn, oss = _setup_connection() - ipv6_object = objects.IP.create(ip=ipv6_address, mac=NULL_MAC, configure_for_dhcp=False) - ipv4_object = objects.IP.create(ip=ipv4_address, mac=NULL_MAC, configure_for_dhcp=False) + ipv6_object = objects.IP.create(ip=str(ipv6_address), mac=NULL_MAC, configure_for_dhcp=False) + ipv4_object = objects.IP.create(ip=str(ipv4_address), mac=NULL_MAC, configure_for_dhcp=False) dns_view = getattr(oss, service_type).dns_view network_view = getattr(oss, service_type).network_view diff --git a/gso/services/provisioning_proxy.py b/gso/services/lso_client.py similarity index 77% rename from gso/services/provisioning_proxy.py rename to gso/services/lso_client.py index 7aba02a4c01b93c476773c0d56f4d86445ce2cf0..67e4b77cce1e4c03f1ba200b6633079cb2476437 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/lso_client.py @@ -1,4 +1,4 @@ -"""The Provisioning Proxy service, which interacts with :term:`LSO` running externally. +"""The :term:`LSO` client, which interacts with :term:`LSO` running externally. :term:`LSO` is responsible for executing Ansible playbooks, that deploy subscriptions. """ @@ -15,7 +15,7 @@ from orchestrator.utils.errors import ProcessFailureError from orchestrator.workflow import Step, StepList, begin, callback_step, inputstep from pydantic_forms.core import FormPage, ReadOnlyField from pydantic_forms.types import FormGenerator -from pydantic_forms.validators import LongText +from pydantic_forms.validators import Label, LongText from gso import settings @@ -33,7 +33,7 @@ def _send_request(parameters: dict, callback_route: str) -> None: :rtype: None """ oss = settings.load_oss_params() - pp_params = oss.PROVISIONING_PROXY + params = oss.PROVISIONING_PROXY # Build up a callback URL of the Provisioning Proxy to return its results to. callback_url = f"{oss.GENERAL.public_hostname}{callback_route}" @@ -41,7 +41,7 @@ def _send_request(parameters: dict, callback_route: str) -> None: logger.debug(debug_msg) parameters.update({"callback": callback_url}) - url = f"{pp_params.scheme}://{pp_params.api_base}/api/playbook" + url = f"{params.scheme}://{params.api_base}/api/playbook" request = requests.post(url, json=parameters, timeout=10) request.raise_for_status() @@ -107,7 +107,7 @@ def execute_playbook( @step("Evaluate provisioning proxy result") -def _evaluate_pp_results(callback_result: dict) -> State: +def _evaluate_results(callback_result: dict) -> State: if callback_result["return_code"] != 0: raise ProcessFailureError(message="Provisioning proxy failure", details=callback_result) @@ -115,34 +115,40 @@ def _evaluate_pp_results(callback_result: dict) -> State: @step("Ignore provisioning proxy result") -def _ignore_pp_results(callback_result: dict) -> State: +def _ignore_results(callback_result: dict) -> State: return {"callback_result": callback_result} @inputstep("Confirm provisioning proxy results", assignee=Assignee("SYSTEM")) -def _show_pp_results(state: State) -> FormGenerator: +def _show_results(state: State) -> FormGenerator: if "callback_result" not in state: return state class ConfirmRunPage(FormPage): class Config: - title: str = f"Execution for {state['subscription']['product']['name']} completed." + title: str = state["lso_result_title"] + if "lso_result_extra_label" in state: + extra_label: Label = state["lso_result_extra_label"] run_status: str = ReadOnlyField(state["callback_result"]["status"]) run_results: LongText = ReadOnlyField(json.dumps(state["callback_result"], indent=4)) yield ConfirmRunPage - state.pop("run_results") + [state.pop(key, None) for key in ["run_results", "lso_result_title", "lso_result_extra_label"]] return state -def pp_interaction(provisioning_step: Step) -> StepList: +def lso_interaction(provisioning_step: Step) -> StepList: """Interact with the provisioning proxy :term:`LSO` using a callback step. An asynchronous interaction with the provisioning proxy. This is an external system that executes Ansible playbooks to provision service subscriptions. If the playbook fails, this step will also fail, allowing for the user to retry provisioning from the UI. + Optionally, the keys ``lso_result_title`` and ``lso_result_extra_label`` can be added to the state before running + this interaction. They will be used to customise the input step that shows the outcome of the :term:`LSO` + interaction. + :param provisioning_step: A workflow step that performs an operation remotely using the provisioning proxy. :type provisioning_step: :class:`Step` :return: A list of steps that is executed as part of the workflow. @@ -150,20 +156,17 @@ def pp_interaction(provisioning_step: Step) -> StepList: """ return ( begin - >> callback_step( - name=provisioning_step.name, - action_step=provisioning_step, - validate_step=_evaluate_pp_results, - ) - >> _show_pp_results + >> callback_step(name=provisioning_step.name, action_step=provisioning_step, validate_step=_evaluate_results) + >> step("Inject result title")(lambda: {"lso_result_title": provisioning_step.name}) + >> _show_results ) -def indifferent_pp_interaction(provisioning_step: Step) -> StepList: +def indifferent_lso_interaction(provisioning_step: Step) -> StepList: """Interact with the provisioning proxy :term:`LSO` using a callback step. - This interaction is identical from the one described in ``pp_interaction()``, with one functional difference. - Whereas the ``pp_interaction()`` will make the workflow step fail on unsuccessful interaction, this step will not. + This interaction is identical from the one described in ``lso_interaction()``, with one functional difference. + Whereas the ``lso_interaction()`` will make the workflow step fail on unsuccessful interaction, this step will not. It is therefore indifferent about the outcome of the Ansible playbook that is executed. .. warning:: @@ -177,10 +180,7 @@ def indifferent_pp_interaction(provisioning_step: Step) -> StepList: """ return ( begin - >> callback_step( - name=provisioning_step.name, - action_step=provisioning_step, - validate_step=_ignore_pp_results, - ) - >> _show_pp_results + >> callback_step(name=provisioning_step.name, action_step=provisioning_step, validate_step=_ignore_results) + >> step("Inject result title")(lambda: {"lso_result_title": provisioning_step.name}) + >> _show_results ) diff --git a/gso/services/partners.py b/gso/services/partners.py new file mode 100644 index 0000000000000000000000000000000000000000..f2080380e30773156af456daa080ae1e37f0a7bc --- /dev/null +++ b/gso/services/partners.py @@ -0,0 +1,67 @@ +"""A module that returns the partners available in :term:`GSO`.""" + +from typing import Any + +from orchestrator.db import db +from sqlalchemy.exc import NoResultFound, SQLAlchemyError + +from gso.db.models import PartnerTable +from gso.schema.partner import PartnerCreate + + +class PartnerNotFoundError(Exception): + """Exception raised when a partner is not found.""" + + +def get_all_partners() -> list[dict]: + """Fetch all partners from the database and serialize them to JSON.""" + partners = PartnerTable.query.all() + return [partner.__json__() for partner in partners] + + +def get_partner_by_name(name: str) -> dict[str, Any]: + """Try to get a partner by their name.""" + try: + partner = PartnerTable.query.filter(PartnerTable.name == name).one() + return partner.__json__() + except NoResultFound as e: + msg = f"partner {name} not found" + raise PartnerNotFoundError(msg) from e + + +def create_partner( + partner_data: PartnerCreate, +) -> dict: + """Create a new partner and add it to the database using Pydantic schema for validation. + + :param partner_data: Partner data validated by Pydantic schema. + :return: JSON representation of the created partner. + """ + try: + new_partner = PartnerTable(**partner_data.dict()) + + db.session.add(new_partner) + db.session.commit() + + return new_partner.__json__() + except SQLAlchemyError: + db.session.rollback() + raise + finally: + db.session.close() + + +def delete_partner_by_name(name: str) -> None: + """Delete a partner by their name.""" + try: + partner = PartnerTable.query.filter(PartnerTable.name == name).one() + db.session.delete(partner) + db.session.commit() + except NoResultFound as e: + msg = f"partner {name} not found" + raise PartnerNotFoundError(msg) from e + except SQLAlchemyError: + db.session.rollback() + raise + finally: + db.session.close() diff --git a/gso/settings.py b/gso/settings.py index 2055e96c78e9177aad9f2b06b2871666b4eced70..ced74ba5c01f59555976396dc43ad268bc786c0b 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -21,6 +21,7 @@ class GeneralParams(BaseSettings): public_hostname: str """The hostname that :term:`GSO` is publicly served at, used for building the callback URL that the provisioning proxy uses.""" + isis_high_metric: int class CeleryParams(BaseSettings): @@ -165,6 +166,13 @@ class EmailParams(BaseSettings): smtp_password: str | None +class SharepointParams(BaseSettings): + """Settings for different Sharepoint sites.""" + + # TODO: Stricter typing after Pydantic 2.x upgrade + checklist_site_url: str + + class OSSParams(BaseSettings): """The set of parameters required for running :term:`GSO`.""" @@ -176,6 +184,7 @@ class OSSParams(BaseSettings): CELERY: CeleryParams THIRD_PARTY_API_KEYS: dict[str, str] EMAIL: EmailParams + SHAREPOINT: SharepointParams def load_oss_params() -> OSSParams: diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index c62a6f69aa064bcefe19428e8179ffdbcd24bf2a..46f0e96e201e09fd1e8b3e9973084289e108f075 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -36,6 +36,7 @@ } }, "workflow": { + "activate_iptrunk": "Activate IP Trunk", "activate_router": "Activate router", "confirm_info": "Please verify this form looks correct.", "deploy_twamp": "Deploy TWAMP", diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index 5890d62b76d042eb8d6ff9c95383ab8cbbedf733..522ceaf58587fbfe7ae4555637431c5e90568ca7 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -8,7 +8,8 @@ from orchestrator.types import State, UUIDstr from orchestrator.utils.json import json_dumps from gso.products.product_types.iptrunk import Iptrunk -from gso.services.provisioning_proxy import execute_playbook +from gso.services.lso_client import execute_playbook +from gso.settings import load_oss_params def _deploy_base_config( @@ -62,11 +63,12 @@ def deploy_base_config_real( return {"subscription": subscription} -@step("[COMMIT] Set ISIS metric to 90.000") -def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State: - """Workflow step for setting the :term:`ISIS` metric to 90k as an arbitrarily high value to drain a link.""" +@step("[COMMIT] Set ISIS metric to very high value") +def set_isis_to_max(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State: + """Workflow step for setting the :term:`ISIS` metric to an arbitrarily high value to drain a link.""" old_isis_metric = subscription.iptrunk.iptrunk_isis_metric - subscription.iptrunk.iptrunk_isis_metric = 90000 + params = load_oss_params() + subscription.iptrunk.iptrunk_isis_metric = params.GENERAL.isis_high_metric extra_vars = { "wfo_trunk_json": json.loads(json_dumps(subscription)), "dry_run": False, diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index e9e304d224c5a9c36314a21050bf878846c8ef70..481ca07838d523e366e7c8387442c0a8dba94336 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -3,12 +3,13 @@ import json from uuid import uuid4 +from orchestrator.config.assignee import Assignee from orchestrator.forms import FormPage -from orchestrator.forms.validators import Choice, UniqueConstrainedList +from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList 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, conditional, done, init, step, workflow +from orchestrator.workflow import StepList, conditional, done, init, inputstep, 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 validator @@ -21,12 +22,13 @@ from gso.products.product_blocks.iptrunk import ( IptrunkType, PhyPortCapacity, ) -from gso.products.product_types.iptrunk import IptrunkInactive +from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router from gso.services import infoblox, subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import execute_playbook, pp_interaction +from gso.services.partners import get_partner_by_name +from gso.settings import load_oss_params from gso.utils.helpers import ( LAGMember, available_interfaces_choices, @@ -54,7 +56,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: title = product_name tt_number: str - customer: str = ReadOnlyField("GÉANT") + partner: str = ReadOnlyField("GEANT") geant_s_sid: str iptrunk_description: str iptrunk_type: IptrunkType @@ -177,9 +179,9 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: @step("Create subscription") -def create_subscription(product: UUIDstr, customer: str) -> State: +def create_subscription(product: UUIDstr, partner: str) -> State: """Create a new subscription object in the database.""" - subscription = IptrunkInactive.from_product_id(product, get_customer_by_name(customer)["id"]) + subscription = IptrunkInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) return { "subscription": subscription, @@ -220,13 +222,14 @@ def initialize_subscription( side_b_ae_members: list[dict], ) -> State: """Take all input from the user, and store it in the database.""" + oss_params = load_oss_params() side_a = Router.from_subscription(side_a_node_id).router side_b = Router.from_subscription(side_b_node_id).router subscription.iptrunk.geant_s_sid = geant_s_sid subscription.iptrunk.iptrunk_description = iptrunk_description subscription.iptrunk.iptrunk_type = iptrunk_type subscription.iptrunk.iptrunk_speed = iptrunk_speed - subscription.iptrunk.iptrunk_isis_metric = 90000 + subscription.iptrunk.iptrunk_isis_metric = oss_params.GENERAL.isis_high_metric subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = side_a @@ -398,6 +401,22 @@ def check_ip_trunk_isis( return {"subscription": subscription} +@step("Register DNS records for both sides of the trunk") +def register_dns_records(subscription: IptrunkInactive) -> State: + """Register :term:`DNS` records for both sides of the newly created IPtrunk.""" + for index, side in enumerate(subscription.iptrunk.iptrunk_sides): + fqdn = f"{side.iptrunk_side_ae_iface}-0.{side.iptrunk_side_node.router_fqdn}" + if not (subscription.iptrunk.iptrunk_ipv4_network and subscription.iptrunk.iptrunk_ipv6_network): + msg = f"Missing IP resources in trunk, cannot allocate DNS record for side {fqdn}!" + raise ValueError(msg) + ipv4_addr = subscription.iptrunk.iptrunk_ipv4_network[index] + ipv6_addr = subscription.iptrunk.iptrunk_ipv6_network[index + 1] + + infoblox.create_host_by_ip(fqdn, ipv4_addr, ipv6_addr, "TRUNK", str(subscription.subscription_id)) + + return {"subscription": subscription} + + @step("NextBox integration") def reserve_interfaces_in_netbox(subscription: IptrunkInactive) -> State: """Create the :term:`LAG` interfaces in NetBox and attach the lag interfaces to the physical interfaces.""" @@ -454,6 +473,27 @@ def netbox_allocate_side_b_interfaces(subscription: IptrunkInactive) -> None: _allocate_interfaces_in_netbox(subscription.iptrunk.iptrunk_sides[1]) +@inputstep("Prompt for new Sharepoint checklist", assignee=Assignee.SYSTEM) +def prompt_start_new_checklist(subscription: IptrunkProvisioning) -> FormGenerator: + """Prompt the operator to start a new checklist in Sharepoint for approving this new IP trunk.""" + oss_params = load_oss_params() + + class SharepointPrompt(FormPage): + class Config: + title = "Start new checklist" + + info_label_1: Label = ( + f"Visit {oss_params.SHAREPOINT.checklist_site_url} and start a new Sharepoint checklist for an IPtrunk " # type: ignore[assignment] + f"from {subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn} to " + f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}." + ) + info_label_2: Label = "Once this is done, click proceed to finish the workflow." # type: ignore[assignment] + + yield SharepointPrompt + + return {} + + @workflow( "Create IP trunk", initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), @@ -482,15 +522,17 @@ def create_iptrunk() -> StepList: >> initialize_subscription >> get_info_from_ipam >> reserve_interfaces_in_netbox - >> pp_interaction(provision_ip_trunk_iface_dry) - >> pp_interaction(provision_ip_trunk_iface_real) - >> pp_interaction(check_ip_trunk_connectivity) - >> pp_interaction(provision_ip_trunk_isis_iface_dry) - >> pp_interaction(provision_ip_trunk_isis_iface_real) - >> pp_interaction(check_ip_trunk_isis) + >> lso_interaction(provision_ip_trunk_iface_dry) + >> lso_interaction(provision_ip_trunk_iface_real) + >> lso_interaction(check_ip_trunk_connectivity) + >> lso_interaction(provision_ip_trunk_isis_iface_dry) + >> lso_interaction(provision_ip_trunk_isis_iface_real) + >> lso_interaction(check_ip_trunk_isis) + >> register_dns_records >> side_a_is_nokia(netbox_allocate_side_a_interfaces) >> side_b_is_nokia(netbox_allocate_side_b_interfaces) >> set_status(SubscriptionLifecycle.PROVISIONING) + >> prompt_start_new_checklist >> resync >> done ) diff --git a/gso/workflows/iptrunk/deploy_twamp.py b/gso/workflows/iptrunk/deploy_twamp.py index 8e2e1016b17fbfc6ba5840e8f47a9c831c9ced17..b9003078a016313bab3012cfee8eaf3f11f278ab 100644 --- a/gso/workflows/iptrunk/deploy_twamp.py +++ b/gso/workflows/iptrunk/deploy_twamp.py @@ -10,7 +10,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic import validator from gso.products.product_types.iptrunk import Iptrunk -from gso.services.provisioning_proxy import execute_playbook, pp_interaction +from gso.services.lso_client import execute_playbook, lso_interaction from gso.utils.helpers import validate_tt_number @@ -88,8 +88,8 @@ def deploy_twamp() -> StepList: init >> store_process_subscription(Target.MODIFY) >> unsync - >> pp_interaction(deploy_twamp_dry) - >> pp_interaction(deploy_twamp_real) + >> lso_interaction(deploy_twamp_dry) + >> lso_interaction(deploy_twamp_real) >> resync >> done ) diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 5f1240c56e74c0b16fe76bec4a72b962fbbec7c3..3ee509cb23ea44bc236ba7f7158b65200c9741aa 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -29,8 +29,8 @@ from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.router import Router from gso.services import infoblox from gso.services.infoblox import DeletionError +from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.services.subscriptions import get_active_router_subscriptions from gso.utils.helpers import ( LAGMember, @@ -41,7 +41,7 @@ from gso.utils.helpers import ( validate_tt_number, ) from gso.utils.shared_enums import Vendor -from gso.utils.workflow_steps import set_isis_to_90000 +from gso.utils.workflow_steps import set_isis_to_max def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -220,6 +220,25 @@ def calculate_old_side_data(subscription: Iptrunk, replace_index: int) -> State: return {"old_side_data": old_side_data} +@step("Check Optical levels on the trunk endpoint") +def check_ip_trunk_optical_levels( + subscription: Iptrunk, + callback_route: str, + replace_index: int, +) -> State: + """Check Optical levels on the trunk.""" + extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "optical"} + + execute_playbook( + playbook_name="iptrunks_checks.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + @step("[DRY RUN] Disable configuration on old router") def disable_old_config_dry( subscription: Iptrunk, @@ -385,6 +404,25 @@ def confirm_continue_move_fiber() -> FormGenerator: return {} +@step("Check IP connectivity of the trunk") +def check_ip_trunk_connectivity( + subscription: Iptrunk, + callback_route: str, + replace_index: int, +) -> State: + """Check successful connectivity across the new trunk.""" + extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "ping"} + + execute_playbook( + playbook_name="iptrunks_checks.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + @step("Deploy ISIS configuration on new router") def deploy_new_isis( subscription: Iptrunk, @@ -422,6 +460,25 @@ def deploy_new_isis( return {"subscription": subscription} +@step("Check ISIS adjacency") +def check_ip_trunk_isis( + subscription: Iptrunk, + callback_route: str, + replace_index: int, +) -> State: + """Run an Ansible playbook to confirm :term:`ISIS` adjacency.""" + extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "isis"} + + execute_playbook( + playbook_name="iptrunks_checks.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + @inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) def confirm_continue_restore_isis() -> FormGenerator: """Wait for an operator to confirm that the old :term:`ISIS` metric should be restored.""" @@ -542,26 +599,26 @@ def delete_old_config_real( @step("Update IP records in IPAM") -def update_ipam(subscription: Iptrunk, old_side_data: dict, new_node: Router, new_lag_interface: str) -> State: +def update_ipam(subscription: Iptrunk, replace_index: int, new_node: Router, new_lag_interface: str) -> State: """Update :term:`IPAM` resources. Move the DNS record pointing to the old side of the trunk, to the new side. """ - old_fqdn = f"{old_side_data['iptrunk_side_ae_iface']}.{old_side_data['iptrunk_side_node']['router_fqdn']}" - trunk_v4 = infoblox.find_host_by_fqdn(old_fqdn) - trunk_v6 = infoblox.find_v6_host_by_fqdn(old_fqdn) + v4_addr = subscription.iptrunk.iptrunk_ipv4_network[replace_index] + # IPv6 networks start with an unused address we need to skip past. + v6_addr = subscription.iptrunk.iptrunk_ipv6_network[replace_index + 1] # Out with the old try: - infoblox.delete_host_by_fqdn(old_fqdn) + infoblox.delete_host_by_ip(subscription.iptrunk.iptrunk_ipv4_network[replace_index]) except DeletionError as e: msg = "Failed to delete record from Infoblox." raise ProcessFailureError(msg) from e # And in with the new - new_fqdn = f"{new_lag_interface}.{new_node.router.router_fqdn}" + new_fqdn = f"{new_lag_interface}-0.{new_node.router.router_fqdn}" comment = str(subscription.subscription_id) - infoblox.create_host_by_ip(new_fqdn, trunk_v4.ipv4addr, trunk_v6.ipv6addr, service_type="TRUNK", comment=comment) + infoblox.create_host_by_ip(new_fqdn, v4_addr, v6_addr, "TRUNK", comment) return {"subscription": subscription} @@ -656,17 +713,21 @@ def migrate_iptrunk() -> StepList: >> unsync >> new_side_is_nokia(netbox_reserve_interfaces) >> calculate_old_side_data - >> pp_interaction(set_isis_to_90000) - >> pp_interaction(disable_old_config_dry) - >> pp_interaction(disable_old_config_real) - >> pp_interaction(deploy_new_config_dry) - >> pp_interaction(deploy_new_config_real) + >> lso_interaction(set_isis_to_max) + >> lso_interaction(check_ip_trunk_optical_levels) + >> lso_interaction(disable_old_config_dry) + >> lso_interaction(disable_old_config_real) + >> lso_interaction(deploy_new_config_dry) + >> lso_interaction(deploy_new_config_real) >> confirm_continue_move_fiber - >> pp_interaction(deploy_new_isis) + >> lso_interaction(check_ip_trunk_optical_levels) + >> lso_interaction(check_ip_trunk_connectivity) + >> lso_interaction(deploy_new_isis) + >> lso_interaction(check_ip_trunk_isis) >> should_restore_isis_metric(confirm_continue_restore_isis) - >> should_restore_isis_metric(pp_interaction(restore_isis_metric)) - >> pp_interaction(delete_old_config_dry) - >> pp_interaction(delete_old_config_real) + >> should_restore_isis_metric(lso_interaction(restore_isis_metric)) + >> lso_interaction(delete_old_config_dry) + >> lso_interaction(delete_old_config_real) >> update_ipam >> update_subscription_model >> old_side_is_nokia(netbox_remove_old_interfaces) diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py index 8c4ade919b7becdf9a4c4690e2d6f610ae453508..55fea705f8af6595933df3786a96de4bf962b953 100644 --- a/gso/workflows/iptrunk/modify_isis_metric.py +++ b/gso/workflows/iptrunk/modify_isis_metric.py @@ -11,7 +11,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.iptrunk import Iptrunk -from gso.services.provisioning_proxy import execute_playbook, pp_interaction +from gso.services.lso_client import execute_playbook, lso_interaction def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -108,8 +108,8 @@ def modify_isis_metric() -> StepList: >> store_process_subscription(Target.MODIFY) >> unsync >> modify_iptrunk_subscription - >> pp_interaction(provision_ip_trunk_isis_iface_dry) - >> pp_interaction(provision_ip_trunk_isis_iface_real) + >> lso_interaction(provision_ip_trunk_isis_iface_dry) + >> lso_interaction(provision_ip_trunk_isis_iface_real) >> resync >> done ) diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index d4e6edb19160c77a429f5a7f307b5897ec5df055..86b43eb55955857148c9184ae9963858bced337c 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -22,8 +22,8 @@ from gso.products.product_blocks.iptrunk import ( PhyPortCapacity, ) from gso.products.product_types.iptrunk import Iptrunk +from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.utils.helpers import ( LAGMember, available_interfaces_choices, @@ -401,8 +401,8 @@ def modify_trunk_interface() -> StepList: >> modify_iptrunk_subscription >> side_a_is_nokia(netbox_update_interfaces_side_a) >> side_b_is_nokia(netbox_update_interfaces_side_b) - >> pp_interaction(provision_ip_trunk_iface_dry) - >> pp_interaction(provision_ip_trunk_iface_real) + >> lso_interaction(provision_ip_trunk_iface_dry) + >> lso_interaction(provision_ip_trunk_iface_real) >> side_a_is_nokia(allocate_interfaces_in_netbox_side_a) >> side_b_is_nokia(allocate_interfaces_in_netbox_side_b) >> resync diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index 330c90c3fddb35253180b8668d61947f484ce9b7..469491ebde407ddb5d92e9ffbb32e96fbb12d994 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -21,11 +21,11 @@ from pydantic import validator from gso.products.product_blocks.iptrunk import IptrunkSideBlock from gso.products.product_types.iptrunk import Iptrunk from gso.services import infoblox +from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.utils.helpers import get_router_vendor, validate_tt_number from gso.utils.shared_enums import Vendor -from gso.utils.workflow_steps import set_isis_to_90000 +from gso.utils.workflow_steps import set_isis_to_max def initial_input_form_generator() -> FormGenerator: @@ -171,9 +171,9 @@ def terminate_iptrunk() -> StepList: config_steps = ( init - >> pp_interaction(set_isis_to_90000) - >> pp_interaction(deprovision_ip_trunk_dry) - >> pp_interaction(deprovision_ip_trunk_real) + >> lso_interaction(set_isis_to_max) + >> lso_interaction(deprovision_ip_trunk_dry) + >> lso_interaction(deprovision_ip_trunk_real) ) ipam_steps = init >> deprovision_ip_trunk_ipv4 >> deprovision_ip_trunk_ipv6 diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index f33dcec56f8314c2ebd188f7e5d9df6865caa032..289f201bd45b3064ad352c733a7c81aa1b7b7462 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -14,12 +14,13 @@ from pydantic import validator from pydantic_forms.core import ReadOnlyField from gso.products.product_blocks.router import RouterRole -from gso.products.product_types.router import RouterInactive +from gso.products.product_types.router import RouterInactive, RouterProvisioning from gso.products.product_types.site import Site from gso.services import infoblox, subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.lso_client import lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import pp_interaction +from gso.services.partners import get_partner_by_name +from gso.settings import load_oss_params from gso.utils.helpers import generate_fqdn, iso_from_ipv4 from gso.utils.shared_enums import PortNumber, Vendor from gso.utils.workflow_steps import deploy_base_config_dry, deploy_base_config_real, run_checks_after_base_config @@ -42,7 +43,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: title = product_name tt_number: str - customer: str = ReadOnlyField("GÉANT") + partner: str = ReadOnlyField("GEANT") vendor: Vendor router_site: _site_selector() # type: ignore[valid-type] hostname: str @@ -70,9 +71,9 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: @step("Create subscription") -def create_subscription(product: UUIDstr, customer: str) -> State: +def create_subscription(product: UUIDstr, partner: str) -> State: """Create a new subscription object.""" - subscription = RouterInactive.from_product_id(product, get_customer_by_name(customer)["id"]) + subscription = RouterInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) return { "subscription": subscription, @@ -203,6 +204,45 @@ def prompt_insert_in_ims() -> FormGenerator: return {} +@inputstep("Prompt RADIUS insertion", assignee=Assignee.SYSTEM) +def prompt_insert_in_radius(subscription: RouterInactive) -> FormGenerator: + """Wait for confirmation from an operator that the router has been inserted in RADIUS.""" + + class RadiusPrompt(FormPage): + class Config: + title = "Update RADIUS clients" + + info_label_1: Label = ( + f"Please go to https://kratos.geant.org/add_radius_client and add the {subscription.router.router_fqdn}" # type: ignore[assignment] + f" - {subscription.router.router_lo_ipv4_address} to radius authentication" + ) + info_label_2: Label = "This will be functionally checked later during verification work." # type: ignore[assignment] + + yield RadiusPrompt + + return {} + + +@inputstep("Prompt for new Sharepoint checklist", assignee=Assignee.SYSTEM) +def prompt_start_new_checklist(subscription: RouterProvisioning) -> FormGenerator: + """Prompt the operator to start a new checklist in Sharepoint for approving this new router.""" + oss_params = load_oss_params() + + class SharepointPrompt(FormPage): + class Config: + title = "Start new checklist" + + info_label_1: Label = ( + f"Visit {oss_params.SHAREPOINT.checklist_site_url} and start a new Sharepoint checklist for " + f"{subscription.router.router_fqdn}." # type: ignore[assignment] + ) + info_label_2: Label = "Once this is done, click proceed to finish the workflow." # type: ignore[assignment] + + yield SharepointPrompt + + return {} + + @workflow( "Create router", initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), @@ -225,15 +265,17 @@ def create_router() -> StepList: >> store_process_subscription(Target.CREATE) >> initialize_subscription >> ipam_allocate_loopback - >> pp_interaction(deploy_base_config_dry) - >> pp_interaction(deploy_base_config_real) + >> lso_interaction(deploy_base_config_dry) + >> lso_interaction(deploy_base_config_real) >> verify_ipam_loopback >> prompt_reboot_router >> prompt_console_login >> prompt_insert_in_ims + >> prompt_insert_in_radius >> router_is_nokia(create_netbox_device) - >> pp_interaction(run_checks_after_base_config) + >> lso_interaction(run_checks_after_base_config) >> set_status(SubscriptionLifecycle.PROVISIONING) + >> prompt_start_new_checklist >> resync >> done ) diff --git a/gso/workflows/router/redeploy_base_config.py b/gso/workflows/router/redeploy_base_config.py index 9c9fc3efeb4d4b9fafdc050140eb856dbe3f103d..ffacc0ce7f1e383ac258971a78601bf39ac6af89 100644 --- a/gso/workflows/router/redeploy_base_config.py +++ b/gso/workflows/router/redeploy_base_config.py @@ -9,7 +9,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.router import Router -from gso.services.provisioning_proxy import pp_interaction +from gso.services.lso_client import lso_interaction from gso.utils.workflow_steps import deploy_base_config_dry, deploy_base_config_real @@ -40,8 +40,8 @@ def redeploy_base_config() -> StepList: init >> store_process_subscription(Target.MODIFY) >> unsync - >> pp_interaction(deploy_base_config_dry) - >> pp_interaction(deploy_base_config_real) + >> lso_interaction(deploy_base_config_dry) + >> lso_interaction(deploy_base_config_real) >> resync >> done ) diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index 20ac88dd053c7d231cdf8445d186f02ee81fa2f8..8abc8877367f5c9333852de0e19720f6f01d25d8 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -20,8 +20,8 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.router import Router from gso.services import infoblox +from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient -from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.utils.shared_enums import Vendor logger = logging.getLogger(__name__) @@ -123,8 +123,8 @@ def terminate_router() -> StepList: >> store_process_subscription(Target.TERMINATE) >> unsync >> run_ipam_steps(deprovision_loopback_ips) - >> run_config_steps(pp_interaction(remove_config_from_router_dry)) - >> run_config_steps(pp_interaction(remove_config_from_router_real)) + >> run_config_steps(lso_interaction(remove_config_from_router_dry)) + >> run_config_steps(lso_interaction(remove_config_from_router_real)) >> router_is_nokia(remove_device_from_netbox) >> set_status(SubscriptionLifecycle.TERMINATED) >> resync diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py index d8ba52a09b36735bd46404a091f7f408ad4f2f66..80f63d21fbcf9c4a87545364504198f3fe67cde4 100644 --- a/gso/workflows/router/update_ibgp_mesh.py +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -14,8 +14,8 @@ from pydantic import root_validator from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router -from gso.services import librenms_client, provisioning_proxy, subscriptions -from gso.services.provisioning_proxy import pp_interaction +from gso.services import librenms_client, lso_client, subscriptions +from gso.services.lso_client import lso_interaction from gso.services.subscriptions import get_active_trunks_that_terminate_on_router from gso.utils.helpers import SNMPVersion @@ -86,7 +86,7 @@ def add_p_to_mesh_dry( "verb": "add_p_to_pe", } - provisioning_proxy.execute_playbook( + lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=_generate_pe_inventory(pe_router_list), @@ -106,7 +106,7 @@ def add_p_to_mesh_real( "verb": "add_p_to_pe", } - provisioning_proxy.execute_playbook( + lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=_generate_pe_inventory(pe_router_list), @@ -134,7 +134,7 @@ def add_all_pe_to_p_dry( "verb": "add_pe_to_p", } - provisioning_proxy.execute_playbook( + lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=subscription["router"]["router_fqdn"], @@ -162,7 +162,7 @@ def add_all_pe_to_p_real( "verb": "add_pe_to_p", } - provisioning_proxy.execute_playbook( + lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=subscription["router"]["router_fqdn"], @@ -173,7 +173,7 @@ def add_all_pe_to_p_real( @step("Verify iBGP session health") def check_ibgp_session(subscription: Router, callback_route: str) -> None: """Run a playbook using the provisioning proxy, to check the health of the new iBGP session.""" - provisioning_proxy.execute_playbook( + lso_client.execute_playbook( playbook_name="check_ibgp.yaml", callback_route=callback_route, inventory=subscription.router.router_fqdn, @@ -247,11 +247,11 @@ def update_ibgp_mesh() -> StepList: >> store_process_subscription(Target.MODIFY) >> unsync >> calculate_pe_router_list - >> pp_interaction(add_p_to_mesh_dry) - >> pp_interaction(add_p_to_mesh_real) - >> pp_interaction(add_all_pe_to_p_dry) - >> pp_interaction(add_all_pe_to_p_real) - >> pp_interaction(check_ibgp_session) + >> lso_interaction(add_p_to_mesh_dry) + >> lso_interaction(add_p_to_mesh_real) + >> lso_interaction(add_all_pe_to_p_dry) + >> lso_interaction(add_all_pe_to_p_real) + >> lso_interaction(check_ibgp_session) >> add_device_to_librenms >> prompt_insert_in_radius >> prompt_radius_login diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index 8e6e13f8436375b4465959985cc115c1e74c40cc..be9aab537c7fec01550b7f8009925b799a8c1fd9 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -11,7 +11,7 @@ from pydantic_forms.core import ReadOnlyField from gso.products.product_blocks import site as site_pb from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate from gso.products.product_types import site -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.utils.helpers import BaseSiteValidatorModel @@ -22,7 +22,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: class Config: title = product_name - customer: str = ReadOnlyField("GÉANT") + partner: str = ReadOnlyField("GEANT") site_name: str site_city: str site_country: str @@ -40,9 +40,9 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: @step("Create subscription") -def create_subscription(product: UUIDstr, customer: str) -> State: +def create_subscription(product: UUIDstr, partner: str) -> State: """Create a new subscription object in the service database.""" - subscription = site.SiteInactive.from_product_id(product, get_customer_by_name(customer)["id"]) + subscription = site.SiteInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) return { "subscription": subscription, diff --git a/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py index f22852d1af60a74225ee689ec590ad3dea666d2c..877c375e3b88b858c8e3484b8943ef80deed8250 100644 --- a/gso/workflows/tasks/import_iptrunk.py +++ b/gso/workflows/tasks/import_iptrunk.py @@ -16,7 +16,7 @@ from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, I from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router from gso.services import subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.utils.helpers import LAGMember @@ -38,7 +38,7 @@ def initial_input_form_generator() -> FormGenerator: class Config: title = "Import Iptrunk" - customer: str + partner: str geant_s_sid: str iptrunk_description: str iptrunk_type: IptrunkType @@ -65,11 +65,11 @@ def initial_input_form_generator() -> FormGenerator: @step("Create a new subscription") -def create_subscription(customer: str) -> State: +def create_subscription(partner: str) -> State: """Create a new subscription in the service database.""" - customer_id = get_customer_by_name(customer)["id"] + partner_id = get_partner_by_name(partner)["partner_id"] product_id = subscriptions.get_product_id_by_name(ProductType.IP_TRUNK) - subscription = IptrunkInactive.from_product_id(product_id, customer_id) + subscription = IptrunkInactive.from_product_id(product_id, partner_id) return { "subscription": subscription, diff --git a/gso/workflows/tasks/import_office_router.py b/gso/workflows/tasks/import_office_router.py index 9b8de86fa099e328971b2d052098839f6f66ff1b..255c7f3133763609edc443f80d8199cef838882f 100644 --- a/gso/workflows/tasks/import_office_router.py +++ b/gso/workflows/tasks/import_office_router.py @@ -13,17 +13,17 @@ from gso.products import ProductType from gso.products.product_types import office_router from gso.products.product_types.office_router import OfficeRouterInactive from gso.services import subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_site_by_name from gso.utils.shared_enums import PortNumber, Vendor @step("Create subscription") -def create_subscription(customer: str) -> State: +def create_subscription(partner: str) -> State: """Create a new subscription object.""" - customer_id = get_customer_by_name(customer)["id"] + partner_id = get_partner_by_name(partner)["partner_id"] product_id = subscriptions.get_product_id_by_name(ProductType.OFFICE_ROUTER) - subscription = OfficeRouterInactive.from_product_id(product_id, customer_id) + subscription = OfficeRouterInactive.from_product_id(product_id, partner_id) return { "subscription": subscription, @@ -38,7 +38,7 @@ def initial_input_form_generator() -> FormGenerator: class Config: title = "Import an office router" - customer: str + partner: str office_router_site: str office_router_fqdn: str office_router_ts_port: PortNumber diff --git a/gso/workflows/tasks/import_router.py b/gso/workflows/tasks/import_router.py index 8346b973d51bf959c3397f5df73e0285a9474d4d..d284dbc56c9dc628739d918b47b5c7d93f2f1d28 100644 --- a/gso/workflows/tasks/import_router.py +++ b/gso/workflows/tasks/import_router.py @@ -15,18 +15,18 @@ from gso.products.product_blocks.router import RouterRole from gso.products.product_types import router from gso.products.product_types.router import RouterInactive from gso.services import subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_site_by_name from gso.utils.helpers import generate_fqdn from gso.utils.shared_enums import PortNumber, Vendor @step("Create subscription") -def create_subscription(customer: str) -> State: +def create_subscription(partner: str) -> State: """Create a new subscription object.""" - customer_id = get_customer_by_name(customer)["id"] + partner_id = get_partner_by_name(partner)["partner_id"] product_id = subscriptions.get_product_id_by_name(ProductType.ROUTER) - subscription = RouterInactive.from_product_id(product_id, customer_id) + subscription = RouterInactive.from_product_id(product_id, partner_id) return { "subscription": subscription, @@ -41,7 +41,7 @@ def initial_input_form_generator() -> FormGenerator: class Config: title = "Import Router" - customer: str + partner: str router_site: str hostname: str ts_port: int diff --git a/gso/workflows/tasks/import_site.py b/gso/workflows/tasks/import_site.py index dfd56b0d4855337ef3aa03a8f7dd7398e5023216..026ffb1be99abab4f34d04f5cc64d72eba66f1c6 100644 --- a/gso/workflows/tasks/import_site.py +++ b/gso/workflows/tasks/import_site.py @@ -12,19 +12,19 @@ from gso.products import ProductType from gso.products.product_blocks.site import SiteTier from gso.products.product_types.site import SiteInactive from gso.services import subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.workflows.site.create_site import initialize_subscription @step("Create subscription") -def create_subscription(customer: str) -> State: +def create_subscription(partner: str) -> State: """Create a new subscription object in the service database. FIXME: all attributes passed by the input form appear to be unused """ - customer_id = get_customer_by_name(customer)["id"] + partner_id = get_partner_by_name(partner)["partner_id"] product_id: UUID = subscriptions.get_product_id_by_name(ProductType.SITE) - subscription = SiteInactive.from_product_id(product_id, customer_id) + subscription = SiteInactive.from_product_id(product_id, partner_id) return { "subscription": subscription, @@ -49,7 +49,7 @@ def generate_initial_input_form() -> FormGenerator: site_internal_id: int site_tier: SiteTier site_ts_address: str - customer: str + partner: str user_input = yield ImportSite return user_input.dict() diff --git a/gso/workflows/tasks/import_super_pop_switch.py b/gso/workflows/tasks/import_super_pop_switch.py index 5d7aa4ab214f630e4db5fa206fc8650681016df1..aa6c832fde157ce6bcb1e0d6bd73a5e62f4e35c5 100644 --- a/gso/workflows/tasks/import_super_pop_switch.py +++ b/gso/workflows/tasks/import_super_pop_switch.py @@ -13,18 +13,18 @@ from gso.products import ProductType from gso.products.product_types import super_pop_switch from gso.products.product_types.super_pop_switch import SuperPopSwitchInactive from gso.services import subscriptions -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_site_by_name from gso.utils.helpers import generate_fqdn from gso.utils.shared_enums import PortNumber, Vendor @step("Create subscription") -def create_subscription(customer: str) -> State: +def create_subscription(partner: str) -> State: """Create a new subscription object.""" - customer_id = get_customer_by_name(customer)["id"] + partner_id = get_partner_by_name(partner)["partner_id"] product_id = subscriptions.get_product_id_by_name(ProductType.SUPER_POP_SWITCH) - subscription = SuperPopSwitchInactive.from_product_id(product_id, customer_id) + subscription = SuperPopSwitchInactive.from_product_id(product_id, partner_id) return { "subscription": subscription, @@ -39,7 +39,7 @@ def initial_input_form_generator() -> FormGenerator: class Config: title = "Import a Super PoP switch" - customer: str + partner: str super_pop_switch_site: str hostname: str super_pop_switch_ts_port: PortNumber diff --git a/setup.py b/setup.py index 5be922344ae8716f0bbbfcd91f73e60b3912fbee..3f5d75dc656e6416e47693fdc10235a230469d77 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,9 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="0.8", - author="GÉANT", - author_email="swd@geant.org", + version="0.9", + author="GÉANT Orchestration and Automation Team", + author_email="goat@geant.org", description="GÉANT Service Orchestrator", url="https://gitlab.software.geant.org/goat/gap/geant-service-orchestrator", packages=find_packages(), diff --git a/test/api/test_imports.py b/test/api/test_imports.py index b4d58f86cef94b2992973cc487fcede995819f14..e1be0d5a74c54b9dc475daf152b9e4ee3f90e4a0 100644 --- a/test/api/test_imports.py +++ b/test/api/test_imports.py @@ -23,7 +23,7 @@ def iptrunk_data(nokia_router_subscription_factory, faker): router_side_a = nokia_router_subscription_factory() router_side_b = nokia_router_subscription_factory() return { - "customer": "GÉANT", + "partner": "GEANT", "geant_s_sid": faker.geant_sid(), "iptrunk_type": IptrunkType.DARK_FIBER, "iptrunk_description": faker.sentence(), @@ -107,7 +107,7 @@ def site_data(faker): "site_internal_id": faker.pyint(), "site_tier": SiteTier.TIER1, "site_ts_address": faker.ipv4(), - "customer": "GÉANT", + "partner": "GEANT", } @@ -120,7 +120,7 @@ def router_data(faker, site_data): "router_vendor": Vendor.JUNIPER, "router_site": site_data["site_name"], "ts_port": 1234, - "customer": "GÉANT", + "partner": "GEANT", "router_lo_ipv4_address": mock_ipv4, "router_lo_ipv6_address": faker.ipv6(), "router_lo_iso_address": iso_from_ipv4(mock_ipv4), @@ -134,7 +134,7 @@ def super_pop_switch_data(faker, site_data): "hostname": "127.0.0.1", "super_pop_switch_site": site_data["site_name"], "super_pop_switch_ts_port": 1234, - "customer": "GÉANT", + "partner": "GEANT", "super_pop_switch_mgmt_ipv4_address": mock_ipv4, } @@ -145,7 +145,7 @@ def office_router_data(faker, site_data): "office_router_fqdn": "127.0.0.1", "office_router_site": site_data["site_name"], "office_router_ts_port": 1234, - "customer": "GÉANT", + "partner": "GEANT", "office_router_lo_ipv4_address": faker.ipv4(), "office_router_lo_ipv6_address": faker.ipv6(), } @@ -235,8 +235,8 @@ def test_import_iptrunk_successful_with_real_process(test_client, mock_routers, @patch("gso.api.v1.imports._start_process") -def test_import_iptrunk_invalid_customer(mock_start_process, test_client, mock_routers, iptrunk_data): - iptrunk_data["customer"] = "not_existing_customer" +def test_import_iptrunk_invalid_partner(mock_start_process, test_client, mock_routers, iptrunk_data): + iptrunk_data["partner"] = "not_existing_partner" mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000" response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data) @@ -244,8 +244,8 @@ def test_import_iptrunk_invalid_customer(mock_start_process, test_client, mock_r assert response.json() == { "detail": [ { - "loc": ["body", "customer"], - "msg": "Customer not_existing_customer not found", + "loc": ["body", "partner"], + "msg": "partner not_existing_partner not found", "type": "value_error", }, ], diff --git a/test/api/test_subscriptions.py b/test/api/test_subscriptions.py index b4bad4e091301ccd644a417f9cf7391779d1d32a..37c74cd83bb24dfa32f338235983fb77ee7feee7 100644 --- a/test/api/test_subscriptions.py +++ b/test/api/test_subscriptions.py @@ -11,7 +11,7 @@ def test_router_subscriptions_endpoint_with_valid_api_key(test_client, nokia_rou nokia_router_subscription_factory(status=SubscriptionLifecycle.INITIAL) response = test_client.get( - ROUTER_SUBSCRIPTION_ENDPOINT, headers={"Authorization": "Bearer REALY_random_AND_3cure_T0keN"} + ROUTER_SUBSCRIPTION_ENDPOINT, headers={"Authorization": "Bearer another_REALY_random_AND_3cure_T0keN"} ) assert response.status_code == 200 diff --git a/test/conftest.py b/test/conftest.py index b0e8ddbd1ff3a99521d1540d92f2ad519f253131..96b4a64056c621b43e3dd724829bcda4835b3fe4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,9 +1,7 @@ import contextlib -import json +import ipaddress import logging import os -import socket -import tempfile from pathlib import Path import orchestrator @@ -22,7 +20,10 @@ from sqlalchemy.orm import scoped_session, sessionmaker from starlette.testclient import TestClient from gso.auth.settings import oauth2lib_settings +from gso.db.models import PartnerType from gso.main import init_gso_app +from gso.schema.partner import PartnerCreate +from gso.services.partners import create_partner from gso.utils.helpers import LAGMember logging.getLogger("faker.factory").setLevel(logging.WARNING) @@ -44,6 +45,23 @@ class UseJuniperSide(strEnum): class FakerProvider(BaseProvider): + def ipv4_network(self, *, min_subnet=1, max_subnet=32) -> ipaddress.IPv4Network: + subnet = str(self.generator.random_int(min=min_subnet, max=max_subnet)) + ipv4 = self.generator.ipv4() + interface = ipaddress.IPv4Interface(ipv4 + "/" + subnet) + # Extra step for converting ``10.53.92.39/24`` to ``10.53.92.0/24`` + network = interface.network.network_address + + return ipaddress.IPv4Network(str(network) + "/" + subnet) + + def ipv6_network(self, *, min_subnet=1, max_subnet=128) -> ipaddress.IPv6Network: + subnet = str(self.generator.random_int(min=min_subnet, max=max_subnet)) + ipv6 = self.generator.ipv6() + interface = ipaddress.IPv6Interface(ipv6 + "/" + subnet) + network = interface.network.network_address + + return ipaddress.IPv6Network(str(network) + "/" + subnet) + def tt_number(self) -> str: random_date = self.generator.date(pattern="%Y%m%d") random_int = self.generator.random_int(min=10000000, max=99999999) @@ -91,149 +109,14 @@ def faker() -> Faker: return fake -@pytest.fixture(scope="session") -def configuration_data() -> dict: - with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - yield { - "GENERAL": {"public_hostname": "https://orchestrator.dev.gap.geant.org"}, - "NETBOX": {"api": "https://127.0.0.1:8000", "token": "TOKEN"}, - "IPAM": { - "INFOBLOX": { - "scheme": "https", - "wapi_version": "2.12", - "host": "10.0.0.1", - "username": "robot-user", - "password": "robot-user-password", - }, - "LO": { - "V4": { - "containers": [], - "networks": ["10.255.255.0/26"], - "mask": 32, - }, - "V6": { - "containers": [], - "networks": ["dead:beef::/80"], - "mask": 128, - }, - "domain_name": ".geant.net", - "dns_view": "default", - "network_view": "default", - }, - "TRUNK": { - "V4": { - "containers": ["10.255.255.0/24", "10.255.254.0/24"], - "networks": [], - "mask": 31, - }, - "V6": { - "containers": ["dead:beef::/64", "dead:beee::/64"], - "networks": [], - "mask": 126, - }, - "domain_name": ".trunk", - "dns_view": "default", - "network_view": "default", - }, - "GEANT_IP": { - "V4": { - "containers": ["10.255.255.0/24", "10.255.254.0/24"], - "networks": [], - "mask": 31, - }, - "V6": { - "containers": ["dead:beef::/64", "dead:beee::/64"], - "networks": [], - "mask": 126, - }, - "domain_name": ".geantip", - "dns_view": "default", - "network_view": "default", - }, - "SI": { - "V4": { - "containers": ["10.255.253.128/25"], - "networks": [], - "mask": 31, - }, - "V6": {"containers": [], "networks": [], "mask": 126}, - "domain_name": ".geantip", - "dns_view": "default", - "network_view": "default", - }, - "LT_IAS": { - "V4": { - "containers": ["10.255.255.0/24"], - "networks": [], - "mask": 31, - }, - "V6": { - "containers": ["dead:beef:cc::/48"], - "networks": [], - "mask": 126, - }, - "domain_name": ".geantip", - "dns_view": "default", - "network_view": "default", - }, - }, - "MONITORING": { - "LIBRENMS": { - "base_url": "http://librenms", - "token": "secret-token", - }, - "SNMP": { - "v2c": { - "community": "fake-community", - }, - "v3": { - "authlevel": "AuthPriv", - "authname": "librenms", - "authpass": "<password1>", - "authalgo": "sha", - "cryptopass": "<password2>", - "cryptoalgo": "aes", - }, - }, - }, - "PROVISIONING_PROXY": { - "scheme": "https", - "api_base": "localhost:44444", - "auth": "Bearer <token>", - "api_version": 1123, - }, - "CELERY": { - "broker_url": "redis://localhost:6379", - "result_backend": "rpc://localhost:6379/0", - "result_expires": 3600, - }, - "THIRD_PARTY_API_KEYS": { - "AnsibleDynamicInventoryGenerator": "REALY_random_AND_3cure_T0keN", - "Application_2": "another_REALY_random_AND_3cure_T0keN", - }, - "EMAIL": { - "from_address": "noreply@nren.local", - "smtp_host": "smtp.nren.local", - "smtp_port": 487, - "starttls_enabled": True, - "smtp_username": "username", - "smtp_password": "password", - }, - } - - @pytest.fixture(scope="session", autouse=True) -def data_config_filename(configuration_data) -> str: - """Create a temporary file with configuration data and set an environment variable to its path.""" - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: - json.dump(configuration_data, f, ensure_ascii=False) - os.environ["OSS_PARAMS_FILENAME"] = f.name +def data_config_filename() -> str: + """Set an environment variable to the path of the example OSS parameters file.""" + config_filename = "gso/oss-params-example.json" - yield f.name + os.environ["OSS_PARAMS_FILENAME"] = config_filename + yield config_filename del os.environ["OSS_PARAMS_FILENAME"] - Path(f.name).unlink() @pytest.fixture(scope="session") @@ -359,3 +242,8 @@ def fastapi_app(_database, db_uri): @pytest.fixture(scope="session") def test_client(fastapi_app): return TestClient(fastapi_app) + + +@pytest.fixture(scope="session") +def geant_partner(): + return create_partner(PartnerCreate(name="GEANT-TEST", partner_type=PartnerType.GEANT)) diff --git a/test/db/__init__.py b/test/db/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/db/test_migrations.py b/test/db/test_migrations.py new file mode 100644 index 0000000000000000000000000000000000000000..cdc6790bf29bbccfd5d928eaa64a00cfceba0918 --- /dev/null +++ b/test/db/test_migrations.py @@ -0,0 +1,16 @@ +import pytest +from alembic.migration import MigrationContext +from sqlalchemy import create_engine + + +@pytest.mark.noautofixt() +def test_migrations_no_branches(db_uri): + engine = create_engine(db_uri) + conn = engine.connect() + context = MigrationContext.configure(conn) + + assert len(context.get_current_heads()) == 1, ( + "\n\n!!!\n\nDetected a branching of db migrations. This is caused by multiple `down_revision`s referring to the" + " same migration.\n\nPlease resolve this by updating either one of these migrations: " + f"{context.get_current_heads()}.\n\n!!!\n\n" + ) diff --git a/test/fixtures.py b/test/fixtures.py index 732439527e0b34f346488fae687dfff98a80a577..0800edec6a175435a7af60afa51cb3b99f1301b7 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -20,11 +20,9 @@ from gso.products.product_types.site import Site, SiteInactive from gso.services import subscriptions from gso.utils.shared_enums import Vendor -CUSTOMER_ID: UUIDstr = "2f47f65a-0911-e511-80d0-005056956c1a" - @pytest.fixture() -def site_subscription_factory(faker): +def site_subscription_factory(faker, geant_partner): def subscription_create( description=None, start_date="2023-05-24T00:00:00+00:00", @@ -38,7 +36,11 @@ def site_subscription_factory(faker): site_internal_id=None, site_tier=SiteTier.TIER1, site_ts_address=None, + partner: dict | None = None, ) -> UUIDstr: + if partner is None: + partner = geant_partner + description = description or "Site Subscription" site_name = site_name or faker.domain_word() site_city = site_city or faker.city() @@ -51,7 +53,7 @@ def site_subscription_factory(faker): site_ts_address = site_ts_address or faker.ipv4() product_id = subscriptions.get_product_id_by_name(ProductType.SITE) - site_subscription = SiteInactive.from_product_id(product_id, customer_id=CUSTOMER_ID, insync=True) + site_subscription = SiteInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) site_subscription.site.site_city = site_city site_subscription.site.site_name = site_name site_subscription.site.site_country = site_country @@ -75,7 +77,7 @@ def site_subscription_factory(faker): @pytest.fixture() -def nokia_router_subscription_factory(site_subscription_factory, faker): +def nokia_router_subscription_factory(site_subscription_factory, faker, geant_partner): def subscription_create( description=None, start_date="2023-05-24T00:00:00+00:00", @@ -88,7 +90,11 @@ def nokia_router_subscription_factory(site_subscription_factory, faker): router_role=RouterRole.PE, router_site=None, status: SubscriptionLifecycle | None = None, + partner: dict | None = None, ) -> UUIDstr: + if partner is None: + partner = geant_partner + description = description or faker.text(max_nb_chars=30) router_fqdn = router_fqdn or faker.domain_name(levels=4) router_ts_port = router_ts_port or faker.random_int(min=1, max=49151) @@ -99,7 +105,7 @@ def nokia_router_subscription_factory(site_subscription_factory, faker): router_site = router_site or site_subscription_factory() product_id = subscriptions.get_product_id_by_name(ProductType.ROUTER) - router_subscription = RouterInactive.from_product_id(product_id, customer_id=CUSTOMER_ID, insync=True) + router_subscription = RouterInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) router_subscription.router.router_fqdn = router_fqdn router_subscription.router.router_ts_port = router_ts_port router_subscription.router.router_access_via_ts = router_access_via_ts @@ -126,7 +132,7 @@ def nokia_router_subscription_factory(site_subscription_factory, faker): @pytest.fixture() -def juniper_router_subscription_factory(site_subscription_factory, faker): +def juniper_router_subscription_factory(site_subscription_factory, faker, geant_partner): def subscription_create( description=None, start_date="2023-05-24T00:00:00+00:00", @@ -139,7 +145,11 @@ def juniper_router_subscription_factory(site_subscription_factory, faker): router_role=RouterRole.PE, router_site=None, status: SubscriptionLifecycle | None = None, + partner: dict | None = None, ) -> UUIDstr: + if partner is None: + partner = geant_partner + description = description or faker.text(max_nb_chars=30) router_fqdn = router_fqdn or faker.domain_name(levels=4) router_ts_port = router_ts_port or faker.random_int(min=1, max=49151) @@ -150,7 +160,8 @@ def juniper_router_subscription_factory(site_subscription_factory, faker): router_site = router_site or site_subscription_factory() product_id = subscriptions.get_product_id_by_name(ProductType.ROUTER) - router_subscription = RouterInactive.from_product_id(product_id, customer_id=CUSTOMER_ID, insync=True) + + router_subscription = RouterInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) router_subscription.router.router_fqdn = router_fqdn router_subscription.router.router_ts_port = router_ts_port router_subscription.router.router_access_via_ts = router_access_via_ts @@ -215,7 +226,7 @@ def iptrunk_side_subscription_factory(nokia_router_subscription_factory, faker): @pytest.fixture() -def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker): +def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker, geant_partner): def subscription_create( description=None, start_date="2023-05-24T00:00:00+00:00", @@ -228,21 +239,27 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker): iptrunk_ipv6_network=None, iptrunk_sides=None, status: SubscriptionLifecycle | None = None, + partner: dict | None = None, ) -> UUIDstr: + if partner is None: + partner = geant_partner + product_id = subscriptions.get_product_id_by_name(ProductType.IP_TRUNK) description = description or faker.sentence() geant_s_sid = geant_s_sid or faker.geant_sid() iptrunk_description = iptrunk_description or faker.sentence() iptrunk_isis_metric = iptrunk_isis_metric or faker.pyint() - iptrunk_ipv4_network = iptrunk_ipv4_network or faker.ipv4(network=True) - iptrunk_ipv6_network = iptrunk_ipv6_network or faker.ipv6(network=True) + iptrunk_ipv4_network = iptrunk_ipv4_network or faker.ipv4_network(max_subnet=31) + iptrunk_ipv6_network = iptrunk_ipv6_network or faker.ipv6_network(max_subnet=126) iptrunk_minimum_links = 1 iptrunk_side_a = iptrunk_side_subscription_factory() iptrunk_side_b = iptrunk_side_subscription_factory() iptrunk_sides = iptrunk_sides or [iptrunk_side_a, iptrunk_side_b] - iptrunk_subscription = IptrunkInactive.from_product_id(product_id, customer_id=CUSTOMER_ID, insync=True) + iptrunk_subscription = IptrunkInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) iptrunk_subscription.iptrunk.geant_s_sid = geant_s_sid iptrunk_subscription.iptrunk.iptrunk_description = iptrunk_description iptrunk_subscription.iptrunk.iptrunk_type = iptrunk_type diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py index 5470858c105666ac8396b42649c65a02eeef8d0c..a8144aacac102f807324458c14ad0cd04c69c892 100644 --- a/test/workflows/__init__.py +++ b/test/workflows/__init__.py @@ -318,8 +318,8 @@ def user_accept_and_assert_suspended(process_stat, step_log, extra_data=None): return result, step_log -def assert_pp_interaction_success(result: Process, process_stat: ProcessStat, step_log: list): - """Assert a successful pp interaction in a workflow. +def assert_lso_interaction_success(result: Process, process_stat: ProcessStat, step_log: list): + """Assert a successful LSO interaction in a workflow. First, the workflow is awaiting callback. It is resumed but a result from LSO, after which the user submits the confirmation input step. Two assertions are made: the workflow is awaiting callback at first, and suspended when @@ -332,8 +332,8 @@ def assert_pp_interaction_success(result: Process, process_stat: ProcessStat, st return resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) -def assert_pp_interaction_failure(result: Process, process_stat: ProcessStat, step_log: list): - """Assert a failed pp interaction in a workflow. +def assert_lso_interaction_failure(result: Process, process_stat: ProcessStat, step_log: list): + """Assert a failed LSO interaction in a workflow. First, the workflow is awaiting callback. It is resumed by a "failure" result from LSO, after which the workflow is in a failed state. This failed state is also returned. Two assertions are made: the workflow is awaiting callback at diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index b85f6d3fc586949dd07d53f5694e23d251eb69f5..38611a75e4f2ebf513fcd9875e8dac619dc1ff99 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -8,12 +8,15 @@ from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.services.subscriptions import get_product_id_by_name from gso.utils.helpers import LAGMember from gso.utils.shared_enums import Vendor +from test import USER_CONFIRM_EMPTY_FORM from test.services.conftest import MockedNetboxClient from test.workflows import ( assert_complete, - assert_pp_interaction_failure, - assert_pp_interaction_success, + assert_lso_interaction_failure, + assert_lso_interaction_success, + assert_suspended, extract_state, + resume_workflow, run_workflow, ) @@ -97,7 +100,9 @@ def input_form_wizard_data(request, juniper_router_subscription_factory, nokia_r @patch("gso.workflows.iptrunk.create_iptrunk.execute_playbook") @patch("gso.workflows.iptrunk.create_iptrunk.infoblox.allocate_v6_network") @patch("gso.workflows.iptrunk.create_iptrunk.infoblox.allocate_v4_network") +@patch("gso.workflows.iptrunk.create_iptrunk.infoblox.create_host_by_ip") def test_successful_iptrunk_creation_with_standard_lso_result( + mock_create_host, mock_allocate_v4_network, mock_allocate_v6_network, mock_execute_playbook, @@ -108,14 +113,18 @@ def test_successful_iptrunk_creation_with_standard_lso_result( data_config_filename: PathLike, test_client, ): - mock_allocate_v4_network.return_value = faker.ipv4(network=True) - mock_allocate_v6_network.return_value = faker.ipv6(network=True) + mock_create_host.return_value = None + mock_allocate_v4_network.return_value = faker.ipv4_network(max_subnet=31) + mock_allocate_v6_network.return_value = faker.ipv6_network(max_subnet=126) product_id = get_product_id_by_name(ProductType.IP_TRUNK) initial_site_data = [{"product": product_id}, *input_form_wizard_data] result, process_stat, step_log = run_workflow("create_iptrunk", initial_site_data) for _ in range(6): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) @@ -151,16 +160,16 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one( _netbox_client_mock, # noqa: PT019 data_config_filename: PathLike, ): - mock_allocate_v4_network.return_value = faker.ipv4(network=True) - mock_allocate_v6_network.return_value = faker.ipv6(network=True) + mock_allocate_v4_network.return_value = faker.ipv4_network(max_subnet=31) + mock_allocate_v6_network.return_value = faker.ipv6_network(max_subnet=126) product_id = get_product_id_by_name(ProductType.IP_TRUNK) initial_site_data = [{"product": product_id}, *input_form_wizard_data] result, process_stat, step_log = run_workflow("create_iptrunk", initial_site_data) - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_pp_interaction_failure(result, process_stat, step_log) + assert_lso_interaction_failure(result, process_stat, step_log) assert mock_execute_playbook.call_count == 2 @@ -170,7 +179,9 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one( @patch("gso.workflows.iptrunk.create_iptrunk.execute_playbook") @patch("gso.workflows.iptrunk.create_iptrunk.infoblox.allocate_v6_network") @patch("gso.workflows.iptrunk.create_iptrunk.infoblox.allocate_v4_network") +@patch("gso.workflows.iptrunk.create_iptrunk.infoblox.create_host_by_ip") def test_successful_iptrunk_creation_with_juniper_interface_names( + mock_create_host, mock_allocate_v4_network, mock_allocate_v6_network, mock_execute_playbook, @@ -181,13 +192,17 @@ def test_successful_iptrunk_creation_with_juniper_interface_names( _netbox_client_mock, # noqa: PT019 test_client, ): - mock_allocate_v4_network.return_value = faker.ipv4(network=True) - mock_allocate_v6_network.return_value = faker.ipv6(network=True) + mock_create_host.return_value = None + mock_allocate_v4_network.return_value = faker.ipv4_network(max_subnet=31) + mock_allocate_v6_network.return_value = faker.ipv6_network(max_subnet=126) product_id = get_product_id_by_name(ProductType.IP_TRUNK) initial_site_data = [{"product": product_id}, *input_form_wizard_data] result, process_stat, step_log = run_workflow("create_iptrunk", initial_site_data) for _ in range(6): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) diff --git a/test/workflows/iptrunk/test_deploy_twamp.py b/test/workflows/iptrunk/test_deploy_twamp.py index 019c67940fef36883349d79b8fd15cc173e52038..c5592738f530a1769c5fd8dd56e02e426950c6ad 100644 --- a/test/workflows/iptrunk/test_deploy_twamp.py +++ b/test/workflows/iptrunk/test_deploy_twamp.py @@ -5,7 +5,7 @@ import pytest from gso.products import Iptrunk from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, extract_state, run_workflow, ) @@ -26,7 +26,7 @@ def test_iptrunk_deploy_twamp_success( result, process_stat, step_log = run_workflow("deploy_twamp", initial_input_data) for _ in range(2): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) diff --git a/test/workflows/iptrunk/test_migrate_iptrunk.py b/test/workflows/iptrunk/test_migrate_iptrunk.py index 825b9e7c779287e3b42ff11d01413562ff66d44b..5640cd646b75083f44d5bfbe37e21d1bfa9115a9 100644 --- a/test/workflows/iptrunk/test_migrate_iptrunk.py +++ b/test/workflows/iptrunk/test_migrate_iptrunk.py @@ -10,7 +10,7 @@ from test import USER_CONFIRM_EMPTY_FORM from test.conftest import UseJuniperSide from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, assert_suspended, extract_state, resume_workflow, @@ -109,10 +109,8 @@ def interface_lists_are_equal(list1, list2): ) @pytest.mark.workflow() @patch("gso.services.infoblox.create_host_by_ip") -@patch("gso.services.infoblox.find_v6_host_by_fqdn") -@patch("gso.services.infoblox.find_host_by_fqdn") -@patch("gso.services.infoblox.delete_host_by_fqdn") -@patch("gso.services.provisioning_proxy._send_request") +@patch("gso.services.infoblox.delete_host_by_ip") +@patch("gso.services.lso_client._send_request") @patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") @patch("gso.services.netbox_client.NetboxClient.get_available_lags") @patch("gso.services.netbox_client.NetboxClient.create_interface") @@ -131,9 +129,7 @@ def test_migrate_iptrunk_success( mocked_get_available_lags, mocked_get_available_interfaces, mock_execute_playbook, - mock_delete_host_by_fqdn, - mock_find_host_by_fqdn, - mock_find_v6_host_by_fqdn, + mock_delete_host_by_ip, mock_create_host_by_ip, migrate_form_input, data_config_filename: PathLike, @@ -151,19 +147,20 @@ def test_migrate_iptrunk_success( result, process_stat, step_log = run_workflow("migrate_iptrunk", migrate_form_input) - for _ in range(5): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + for _ in range(6): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + for _ in range(4): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) for _ in range(3): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) @@ -172,11 +169,9 @@ def test_migrate_iptrunk_success( subscription = Iptrunk.from_subscription(subscription_id) assert subscription.status == "active" - assert mock_execute_playbook.call_count == 9 - assert mock_find_host_by_fqdn.call_count == 1 - assert mock_find_v6_host_by_fqdn.call_count == 1 + assert mock_execute_playbook.call_count == 13 assert mock_create_host_by_ip.call_count == 1 - assert mock_delete_host_by_fqdn.call_count == 1 + assert mock_delete_host_by_ip.call_count == 1 # get some values from form new_router = migrate_form_input[2]["new_node"] diff --git a/test/workflows/iptrunk/test_modify_isis_metric.py b/test/workflows/iptrunk/test_modify_isis_metric.py index 914998552a4dfe3b88c884138cb281b88712f01c..38f4b4e89e5d178b86c48eaafc32a311f07787c2 100644 --- a/test/workflows/iptrunk/test_modify_isis_metric.py +++ b/test/workflows/iptrunk/test_modify_isis_metric.py @@ -5,14 +5,14 @@ import pytest from gso.products import Iptrunk from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, extract_state, run_workflow, ) @pytest.mark.workflow() -@patch("gso.services.provisioning_proxy.execute_playbook") +@patch("gso.services.lso_client.execute_playbook") def test_iptrunk_modify_isis_metric_success( mock_provision_ip_trunk, iptrunk_subscription_factory, @@ -30,7 +30,7 @@ def test_iptrunk_modify_isis_metric_success( result, process_stat, step_log = run_workflow("modify_isis_metric", initial_iptrunk_data) for _ in range(2): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 78bedca5562302e2b6e6c12e3e71bbbb93bf7758..372933d78795bdcbc29800caa6ae0dfac6c27340 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -8,7 +8,7 @@ from gso.utils.shared_enums import Vendor from test.conftest import UseJuniperSide from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, extract_state, run_workflow, ) @@ -121,7 +121,7 @@ def test_iptrunk_modify_trunk_interface_success( result, process_stat, step_log = run_workflow("modify_trunk_interface", input_form_iptrunk_data) for _ in range(2): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) diff --git a/test/workflows/iptrunk/test_terminate_iptrunk.py b/test/workflows/iptrunk/test_terminate_iptrunk.py index 68b5f4edd155fa7f6e2760c2c36fec02e939f74f..05a94456ab00aa1145f2e1dae1953eae437568ee 100644 --- a/test/workflows/iptrunk/test_terminate_iptrunk.py +++ b/test/workflows/iptrunk/test_terminate_iptrunk.py @@ -3,10 +3,11 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk +from gso.settings import load_oss_params from test.services.conftest import MockedNetboxClient from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, extract_state, run_workflow, ) @@ -35,6 +36,7 @@ def test_successful_iptrunk_termination( mocked_free_interface.return_value = mocked_netbox.free_interface() # Run workflow + oss_params = load_oss_params() initial_iptrunk_data = [ {"subscription_id": product_id}, { @@ -46,7 +48,7 @@ def test_successful_iptrunk_termination( result, process_stat, step_log = run_workflow("terminate_iptrunk", initial_iptrunk_data) for _ in range(3): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) @@ -62,4 +64,4 @@ def test_successful_iptrunk_termination( assert mock_execute_playbook.call_count == 2 assert mock_set_isis_to_90k.call_count == 1 assert mock_infoblox_delete_network.call_count == 2 - assert subscription.iptrunk.iptrunk_isis_metric == 90000 + assert subscription.iptrunk.iptrunk_isis_metric == oss_params.GENERAL.isis_high_metric diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py index efa7e6732c524cbf1d6edf0584fe207b070efcdf..a6d61729946e11249b8773599edfd3d023499dbb 100644 --- a/test/workflows/router/test_create_router.py +++ b/test/workflows/router/test_create_router.py @@ -11,8 +11,8 @@ from gso.utils.shared_enums import Vendor from test import USER_CONFIRM_EMPTY_FORM from test.workflows import ( assert_complete, - assert_pp_interaction_failure, - assert_pp_interaction_success, + assert_lso_interaction_failure, + assert_lso_interaction_success, assert_suspended, extract_state, resume_workflow, @@ -85,14 +85,17 @@ def test_create_nokia_router_success( ) for _ in range(2): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - # Handle three consecutive user input steps - for _ in range(3): + # Handle four consecutive user input steps + for _ in range(4): assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) @@ -170,13 +173,13 @@ def test_create_nokia_router_lso_failure( initial_router_data = [{"product": product_id}, router_creation_input_form_data] result, process_stat, step_log = run_workflow("create_router", initial_router_data) - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) state = extract_state(result) subscription_id = state["subscription_id"] subscription = Router.from_subscription(subscription_id) - assert_pp_interaction_failure(result, process_stat, step_log) + assert_lso_interaction_failure(result, process_stat, step_log) assert subscription.status == "initial" assert subscription.description == f"Router {mock_fqdn}" diff --git a/test/workflows/router/test_redeploy_base_config.py b/test/workflows/router/test_redeploy_base_config.py index 026999d0e9b23f590c5e434aba24d56e4bc47e91..d624d62eb23875156d550852d6bf28af03722b8a 100644 --- a/test/workflows/router/test_redeploy_base_config.py +++ b/test/workflows/router/test_redeploy_base_config.py @@ -5,14 +5,14 @@ import pytest from gso.products import Router from test.workflows import ( assert_complete, - assert_pp_interaction_success, + assert_lso_interaction_success, extract_state, run_workflow, ) @pytest.mark.workflow() -@patch("gso.services.provisioning_proxy._send_request") +@patch("gso.services.lso_client._send_request") def test_redeploy_base_config_success( mock_provision_router, nokia_router_subscription_factory, @@ -26,7 +26,7 @@ def test_redeploy_base_config_success( result, process_stat, step_log = run_workflow("redeploy_base_config", initial_input_data) for _ in range(2): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) diff --git a/test/workflows/router/test_terminate_router.py b/test/workflows/router/test_terminate_router.py index 25307cc6101b6efa4c4ce3ad18ccc907a118b378..bc978824b1f03f03fa7b06a8f5325f1d26959a27 100644 --- a/test/workflows/router/test_terminate_router.py +++ b/test/workflows/router/test_terminate_router.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest from gso.products import Router -from test.workflows import assert_complete, assert_pp_interaction_success, extract_state, run_workflow +from test.workflows import assert_complete, assert_lso_interaction_success, extract_state, run_workflow @pytest.mark.workflow() @@ -16,7 +16,7 @@ from test.workflows import assert_complete, assert_pp_interaction_success, extra (False, False), ], ) -@patch("gso.services.provisioning_proxy._send_request") +@patch("gso.services.lso_client._send_request") @patch("gso.workflows.router.terminate_router.NetboxClient.delete_device") @patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip") def test_terminate_router_full_success( @@ -36,14 +36,14 @@ def test_terminate_router_full_success( "remove_configuration": remove_configuration, "clean_up_ipam": clean_up_ipam, } - pp_interaction_count = 2 if remove_configuration else 0 + lso_interaction_count = 2 if remove_configuration else 0 # Run workflow initial_router_data = [{"subscription_id": product_id}, router_termination_input_form_data] result, process_stat, step_log = run_workflow("terminate_router", initial_router_data) - for _ in range(pp_interaction_count): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + for _ in range(lso_interaction_count): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) @@ -54,4 +54,4 @@ def test_terminate_router_full_success( assert subscription.status == "terminated" assert mock_delete_device.call_count == 1 assert mock_delete_host_by_ip.call_count == (1 if clean_up_ipam else 0) - assert mock_execute_playbook.call_count == pp_interaction_count + assert mock_execute_playbook.call_count == lso_interaction_count diff --git a/test/workflows/router/test_update_ibgp_mesh.py b/test/workflows/router/test_update_ibgp_mesh.py index 8c7da5d5d0abb2a4275f52e2c89cc2c52bc7f51e..b2f6756b8d7820e8e9dedd9f0f23b644eb86bb67 100644 --- a/test/workflows/router/test_update_ibgp_mesh.py +++ b/test/workflows/router/test_update_ibgp_mesh.py @@ -7,7 +7,13 @@ from pydantic_forms.exceptions import FormValidationError from gso.products import Iptrunk from gso.products.product_blocks.router import RouterRole from test import USER_CONFIRM_EMPTY_FORM -from test.workflows import assert_pp_interaction_success, assert_suspended, extract_state, resume_workflow, run_workflow +from test.workflows import ( + assert_lso_interaction_success, + assert_suspended, + extract_state, + resume_workflow, + run_workflow, +) @pytest.fixture() @@ -18,7 +24,7 @@ def ibgp_mesh_input_form_data(iptrunk_subscription_factory, faker): @pytest.mark.workflow() -@patch("gso.workflows.router.update_ibgp_mesh.provisioning_proxy.execute_playbook") +@patch("gso.workflows.router.update_ibgp_mesh.lso_client.execute_playbook") @patch("gso.workflows.router.update_ibgp_mesh.librenms_client.LibreNMSClient.add_device") def test_update_ibgp_mesh_success( mock_librenms_add_device, @@ -32,7 +38,7 @@ def test_update_ibgp_mesh_success( ) for _ in range(5): - result, step_log = assert_pp_interaction_success(result, process_stat, step_log) + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) # Handle two consecutive user input steps for _ in range(2): diff --git a/test/workflows/site/test_create_site.py b/test/workflows/site/test_create_site.py index 6a260bc111f055b5724af699da61d4d6a145886f..5909b5db9d19fca60d7815befd1a60d7d3676086 100644 --- a/test/workflows/site/test_create_site.py +++ b/test/workflows/site/test_create_site.py @@ -4,7 +4,7 @@ from pydantic_forms.exceptions import FormValidationError from gso.products import ProductType from gso.products.product_blocks.site import SiteTier from gso.products.product_types.site import Site -from gso.services.crm import get_customer_by_name +from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name from test.workflows import assert_complete, extract_state, run_workflow @@ -65,7 +65,7 @@ def test_site_name_is_incorrect(responses, faker): "site_internal_id": faker.pyint(), "site_tier": SiteTier.TIER1, "site_ts_address": faker.ipv4(), - "customer": get_customer_by_name("GÉANT")["id"], + "partner": get_partner_by_name("GEANT")["partner_id"], }, ] diff --git a/tox.ini b/tox.ini index 02437f720ac1159df27281b195faf1f44ff7ed70..de7627e2dd6ed3c6700daff2e5016d50e4128ba0 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = -r requirements.txt commands = - ruff --respect-gitignore --preview . + ruff check --respect-gitignore --preview . ruff format --respect-gitignore --preview --check . mypy . coverage erase