diff --git a/example.csv b/example.csv new file mode 100644 index 0000000000000000000000000000000000000000..8b7a5d92dde262a65d9994f113d162d9a37e91ea --- /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 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 223278f066141b28d7333c214d24d31c40217357..e728884c604043b823b2c1515cb8c03f7978fec0 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 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..11d045ee8a5cd11d48e84dbe97de1e3ceb4dffad --- /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 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-14_d6800280b31a_added_partner_table.py b/gso/migrations/versions/2024-03-14_d6800280b31a_added_partner_table.py new file mode 100644 index 0000000000000000000000000000000000000000..1d10904e5c25b203afb136a53ba35c29b1cfc9b3 --- /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 0000000000000000000000000000000000000000..1a41e97bcb719327136048ed0c1f6e697f7a043f --- /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 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 b417b88acdaa19e86c8bcd6d2d4a75d29ba6ff03..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 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 0000000000000000000000000000000000000000..ea22602845e1f4c13b8170f4b3854c3ee855e064 --- /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 a9c8cda79344ac6fdb03770ca21bebf9a71c9070..88ac9f4403f989acaa5ae7259a70ccacbf877a1b 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 78bd2649b86af7691c992039070717fadf5e6a4d..694b4136c8e851962f9bdd81a361f94fc75c50d9 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 52ed8c95e566a814d0d2c40435fa64a192133e74..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("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 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/test/api/test_imports.py b/test/api/test_imports.py index d3e63ea9c267906a01955357c322ac18b0f0668f..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": "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 54343abf6a6456b2fd91f326ea12dacd1e01532d..96b4a64056c621b43e3dd724829bcda4835b3fe4 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 732439527e0b34f346488fae687dfff98a80a577..3c24cb4d76d7c8c58c2f4fa9826276ad3793af0d 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 54fdbce36f654dba93813debe98d0ee957abf883..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("GEANT")["id"], + "partner": get_partner_by_name("GEANT")["partner_id"], }, ]