From 67d1e814282ce062203f948246f4779aa5ff7f5a Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Mon, 18 Mar 2024 10:27:18 +0100 Subject: [PATCH] added partner model added import script for reading partners from csv files --- example.csv | 5 ++ gso/api/v1/imports.py | 24 +++---- gso/cli/imports.py | 42 ++++++++++- gso/db/__init__.py | 1 + gso/db/models.py | 51 ++++++++++++++ gso/migrations/env.py | 2 + ...11-21_e8378fbcfbf3_add_initial_products.py | 4 +- ...3c90ea1d8c_add_super_pop_switch_product.py | 2 +- ...-03-14_d6800280b31a_added_partner_table.py | 69 +++++++++++++++++++ gso/schema/__init__.py | 1 + gso/schema/partner.py | 30 ++++++++ gso/services/crm.py | 31 --------- gso/services/partners.py | 63 +++++++++++++++++ gso/workflows/iptrunk/create_iptrunk.py | 8 +-- gso/workflows/router/create_router.py | 8 +-- gso/workflows/site/create_site.py | 8 +-- gso/workflows/tasks/import_iptrunk.py | 10 +-- gso/workflows/tasks/import_office_router.py | 10 +-- gso/workflows/tasks/import_router.py | 10 +-- gso/workflows/tasks/import_site.py | 10 +-- .../tasks/import_super_pop_switch.py | 10 +-- test/api/test_imports.py | 18 ++--- test/conftest.py | 8 +++ test/fixtures.py | 35 +++++++--- test/workflows/site/test_create_site.py | 4 +- 25 files changed, 358 insertions(+), 106 deletions(-) create mode 100644 example.csv create mode 100644 gso/db/__init__.py create mode 100644 gso/db/models.py create mode 100644 gso/migrations/versions/2024-03-14_d6800280b31a_added_partner_table.py create mode 100644 gso/schema/__init__.py create mode 100644 gso/schema/partner.py delete mode 100644 gso/services/crm.py create mode 100644 gso/services/partners.py diff --git a/example.csv b/example.csv new file mode 100644 index 00000000..8b7a5d92 --- /dev/null +++ b/example.csv @@ -0,0 +1,5 @@ +name,email,as_number,as_set,route_set,black_listed_as_sets,additional_routers,additional_bgp_speakers,partner_type,created_at +Partner A,partnera@example.com,AS1234,,,"AS100,AS200",,"Router1,Router2",NREN,2024-03-14 +Partner B,partnerb@example.com,AS5678,,,"AS300,AS400",,"Router3,Router4",RE_PEER,2024-03-15 +Partner C,partnerc@example.com,AS91011,,,"AS500,AS600",,"Router5,Router6",PUBLIC_PEER,2024-03-16 + diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index dd77bc80..688e0c05 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 223278f0..e728884c 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 datetime, UTC 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"] = "GEANT" + 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="GEANT", + partner="GEANT", geant_s_sid=trunk["id"], iptrunk_type=trunk["config"]["common"]["type"], iptrunk_description=trunk["config"]["common"].get("description", ""), @@ -197,3 +202,36 @@ 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")): + """Import partners from a CSV file into the database.""" + typer.echo(f"Importing partners from {file_path} ...") + + partners = import_partners_from_csv(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/db/__init__.py b/gso/db/__init__.py new file mode 100644 index 00000000..4e2d8c84 --- /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 00000000..11d045ee --- /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): + """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 4d84cfb1..45dc109d 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 d2fa1e99..12c51d34 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 aac35ee3..df3ddd20 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-14_d6800280b31a_added_partner_table.py b/gso/migrations/versions/2024-03-14_d6800280b31a_added_partner_table.py new file mode 100644 index 00000000..1d10904e --- /dev/null +++ b/gso/migrations/versions/2024-03-14_d6800280b31a_added_partner_table.py @@ -0,0 +1,69 @@ +"""Added Partner table. + +Revision ID: d6800280b31a +Revises: 6e4952687205 +Create Date: 2024-03-14 16:38:55.948838 + +""" +import sqlalchemy as sa +from alembic import op +from orchestrator.db import UtcTimestamp +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd6800280b31a' +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') diff --git a/gso/schema/__init__.py b/gso/schema/__init__.py new file mode 100644 index 00000000..1a41e97b --- /dev/null +++ b/gso/schema/__init__.py @@ -0,0 +1 @@ +"""Partner schema module.""" diff --git a/gso/schema/partner.py b/gso/schema/partner.py new file mode 100644 index 00000000..890adcb9 --- /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 b417b88a..00000000 --- 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 GEANT 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": "GEANT", - }, - ] - - -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/partners.py b/gso/services/partners.py new file mode 100644 index 00000000..ea226028 --- /dev/null +++ b/gso/services/partners.py @@ -0,0 +1,63 @@ +"""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 + + +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 diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index a9c8cda7..88ac9f44 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -25,8 +25,8 @@ from gso.products.product_blocks.iptrunk import ( 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.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_name from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.settings import load_oss_params from gso.utils.helpers import ( @@ -56,7 +56,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: title = product_name tt_number: str - customer: str = ReadOnlyField("GEANT") + partner: str = ReadOnlyField("GEANT") geant_s_sid: str iptrunk_description: str iptrunk_type: IptrunkType @@ -179,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, diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 78bd2649..694b4136 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -17,8 +17,8 @@ from gso.products.product_blocks.router import RouterRole 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.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_name from gso.services.provisioning_proxy import pp_interaction from gso.settings import load_oss_params from gso.utils.helpers import generate_fqdn, iso_from_ipv4 @@ -43,7 +43,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: title = product_name tt_number: str - customer: str = ReadOnlyField("GEANT") + partner: str = ReadOnlyField("GEANT") vendor: Vendor router_site: _site_selector() # type: ignore[valid-type] hostname: str @@ -71,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, diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index 52ed8c95..be9aab53 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("GEANT") + 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 f22852d1..877c375e 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 9b8de86f..255c7f31 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 8346b973..d284dbc5 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 dfd56b0d..026ffb1b 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 5d7aa4ab..aa6c832f 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/test/api/test_imports.py b/test/api/test_imports.py index d3e63ea9..e1be0d5a 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": "GEANT", + "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": "GEANT", + "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": "GEANT", + "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": "GEANT", + "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": "GEANT", + "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/conftest.py b/test/conftest.py index 54343abf..96b4a640 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -20,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) @@ -239,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/fixtures.py b/test/fixtures.py index 73243952..3c24cb4d 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,7 +239,11 @@ 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() @@ -242,7 +257,7 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker): 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/site/test_create_site.py b/test/workflows/site/test_create_site.py index 54fdbce3..5909b5db 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("GEANT")["id"], + "partner": get_partner_by_name("GEANT")["partner_id"], }, ] -- GitLab