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..5e5705667e30b16dce6260a8fd5ded43be10f690
--- /dev/null
+++ b/gso/services/partners.py
@@ -0,0 +1,67 @@
+"""A module that returns the partners available in :term:`GSO`.
+
+For the time being, it's hardcoded to only contain GEANT as a partner, since this is needed for the deployment of phase
+1.
+"""
+
+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..210384d37518942b24202d0629f757a5c1362e13 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)["id"])
return {
"subscription": subscription,
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index 78bd2649b86af7691c992039070717fadf5e6a4d..6c0cef5d8d95d5ad28d47583764c1cab8c2a5aa1 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)["id"])
return {
"subscription": subscription,
diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py
index 52ed8c95e566a814d0d2c40435fa64a192133e74..6083c3938653e44c305e9fdee81746588a76cea2 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)["id"])
return {
"subscription": subscription,
diff --git a/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py
index f22852d1af60a74225ee689ec590ad3dea666d2c..4328162fb81a8b7a2938d417c0c4dcd0dcba5355 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)["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..65863e135cb7ceb28f2ef256430cd6bbd15aa4f3 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)["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..780d162abddc70428ccc42bc39524b6d36ea7ec8 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)["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..c6186cb7e725b7a40931de3284d01f3f5dd38e5c 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)["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..e2ae4e09c4576c79c4ef3fffd174e3f1c10cbff4 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)["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..e7d80b3699bac6e8499be56f7481b845335c6562 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", type=PartnerType.GEANT))
diff --git a/test/fixtures.py b/test/fixtures.py
index 732439527e0b34f346488fae687dfff98a80a577..4ff9e7f95677dec5dc90deea4f112c405134cd82 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["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["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["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["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..8d291b0baaf04d04eea6c0c830e577b372fb60d8 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")["id"],
},
]