diff --git a/gso/__init__.py b/gso/__init__.py index abb76a7cd09d52fe0d266e04a9f5ab9b1d550c3d..59c067be44c520a9034aeddc179b3a98ac65aaf7 100644 --- a/gso/__init__.py +++ b/gso/__init__.py @@ -1,3 +1,5 @@ +"""The main entrypoint for :term:`GSO`, and the different ways in which it can be run.""" + import typer from orchestrator import OrchestratorCore, app_settings from orchestrator.cli.main import app as cli_app @@ -9,16 +11,19 @@ from gso.cli import netbox def init_gso_app() -> OrchestratorCore: + """Initialise the :term:`GSO` app.""" app = OrchestratorCore(base_settings=app_settings) app.include_router(api_router, prefix="/api") return app def init_worker_app() -> OrchestratorCore: + """Initialise a :term:`GSO` instance as Celery worker.""" return OrchestratorCore(base_settings=app_settings) def init_cli_app() -> typer.Typer: + """Initialise :term:`GSO` as a CLI application.""" from gso.cli import import_sites cli_app.add_typer(import_sites.app, name="import_sites") diff --git a/gso/api/__init__.py b/gso/api/__init__.py index f30090d3e1462d787308bd56d6ce5ab675144c40..edcd2c754884f415593b3ea087bfba956c1b9b3d 100644 --- a/gso/api/__init__.py +++ b/gso/api/__init__.py @@ -1,3 +1,4 @@ +"""Initialisation class for the :term:`GSO` :term:`API`.""" from fastapi import APIRouter from gso.api.v1 import router as router_v1 diff --git a/gso/api/v1/__init__.py b/gso/api/v1/__init__.py index 6553f1f83b6a31d91aee49224d72242c937820c8..05d4e8c72b1745e6de787d436a1536fcc94987c0 100644 --- a/gso/api/v1/__init__.py +++ b/gso/api/v1/__init__.py @@ -1,3 +1,4 @@ +"""Version 1 of the :term:`GSO` :term:`API`.""" from fastapi import APIRouter from gso.api.v1.imports import router as imports_router diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index 7088e5a662b93ebe035656e3f4f8f095e0104066..6500238983ddf0b5fb27d6461e36afcc70df64b3 100644 --- a/gso/api/v1/imports.py +++ b/gso/api/v1/imports.py @@ -1,3 +1,4 @@ +""":term:`GSO` :term:`API` endpoints that import different types of existing services.""" import ipaddress from typing import Any from uuid import UUID @@ -26,11 +27,15 @@ router = APIRouter(prefix="/imports", tags=["Imports"], dependencies=[Depends(op class ImportResponseModel(BaseModel): + """The model of a response given when services are imported using the :term:`API`.""" + pid: UUID detail: str class SiteImportModel(BaseModel): + """The required input for importing an existing :class:`gso.products.product_types.site`.""" + site_name: str site_city: str site_country: str @@ -45,17 +50,20 @@ class SiteImportModel(BaseModel): @validator("site_ts_address", allow_reuse=True) def validate_ts_address(cls, site_ts_address: str) -> str: + """A terminal server address must be valid.""" validate_site_fields_is_unique("site_ts_address", site_ts_address) validate_ipv4_or_ipv6(site_ts_address) return site_ts_address @validator("site_country_code", allow_reuse=True) def country_code_must_exist(cls, country_code: str) -> str: + """A country code must exist.""" validate_country_code(country_code) return country_code @validator("site_internal_id", "site_bgp_community_id", allow_reuse=True) def validate_unique_fields(cls, value: str, field: ModelField) -> str | int: + """Validate that the internal side ID and :term:`BGP` community IDs are unique.""" return validate_site_fields_is_unique(field.name, value) @validator("site_name", allow_reuse=True) @@ -71,6 +79,8 @@ class SiteImportModel(BaseModel): class RouterImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.product.product_types.router`.""" + customer: str router_site: str hostname: str @@ -87,6 +97,8 @@ class RouterImportModel(BaseModel): class IptrunkImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.products.product_types.iptrunk`.""" + customer: str geant_s_sid: str iptrunk_type: IptrunkType @@ -114,6 +126,7 @@ class IptrunkImportModel(BaseModel): @validator("customer") def check_if_customer_exists(cls, value: str) -> str: + """The customer must exist.""" try: get_customer_by_name(value) except CustomerNotFoundError as e: @@ -124,6 +137,7 @@ class IptrunkImportModel(BaseModel): @validator("side_a_node_id", "side_b_node_id") def check_if_router_side_is_available(cls, value: str) -> str: + """Both sides of the trunk must exist in :term:`GSO`.""" if value not in cls._get_active_routers(): msg = f"Router {value} not found" raise ValueError(msg) @@ -132,6 +146,7 @@ class IptrunkImportModel(BaseModel): @validator("side_a_ae_members", "side_b_ae_members") def check_side_uniqueness(cls, value: list[str]) -> list[str]: + """:term:`LAG` members must be unique.""" if len(value) != len(set(value)): msg = "Items must be unique" raise ValueError(msg) @@ -140,6 +155,7 @@ class IptrunkImportModel(BaseModel): @root_validator def check_members(cls, values: dict[str, Any]) -> dict[str, Any]: + """Amount of :term:`LAG` members has to match on side A and B, and meet the minimum requirement.""" min_links = values["iptrunk_minimum_links"] side_a_members = values.get("side_a_ae_members", []) side_b_members = values.get("side_b_ae_members", []) @@ -167,7 +183,7 @@ def _start_process(process_name: str, data: dict) -> UUID: detail="Failed to start the process.", ) - process = processes._get_process(pid) + process = processes._get_process(pid) # noqa: SLF001 if process.last_status == "failed": raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/gso/api/v1/subscriptions.py b/gso/api/v1/subscriptions.py index cd0731f2f1694b638ad03130d622efb9323f021a..fee0cbb6303eac9979f631e636d9e898675b9c05 100644 --- a/gso/api/v1/subscriptions.py +++ b/gso/api/v1/subscriptions.py @@ -1,3 +1,4 @@ +""":term:`API` endpoint for fetching different types of subscriptions.""" from typing import Any from fastapi import Depends, status diff --git a/gso/cli/__init__.py b/gso/cli/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b50714544da3a18459f655c47ce01e224a596c00 100644 --- a/gso/cli/__init__.py +++ b/gso/cli/__init__.py @@ -0,0 +1 @@ +"""The :term:`CLI` of :term:`GSO`.""" diff --git a/gso/cli/import_sites.py b/gso/cli/import_sites.py index 36e0cb49f9afffaae7c68119cb635f1199c60321..c897ae7a5ace0fe55256ccb28298b867fbee1302 100644 --- a/gso/cli/import_sites.py +++ b/gso/cli/import_sites.py @@ -1,3 +1,4 @@ +""":term:`CLI` command for importing sites.""" import typer app: typer.Typer = typer.Typer() diff --git a/gso/cli/netbox.py b/gso/cli/netbox.py index 97ee7bc16fedd17f976853faeb88dbd9bb3406f8..958684cb64bccb00c54f0473d16c0d916b4b12b1 100644 --- a/gso/cli/netbox.py +++ b/gso/cli/netbox.py @@ -1,3 +1,4 @@ +"""A :term:`CLI` for interacting with Netbox.""" import typer from pynetbox import RequestError diff --git a/gso/products/__init__.py b/gso/products/__init__.py index b2887f35d7c4fb6c9734bd9800d30fcf58356ff8..9fa0448def558c369223b6032ba59d59184431c5 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -13,6 +13,8 @@ from gso.products.product_types.site import Site class ProductType(strEnum): + """An enumerator of available products in :term:`GSO`.""" + SITE = "Site" ROUTER = "Router" IP_TRUNK = "IP trunk" diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py index 8212a6ec3e7a9ffd90e5eaba92cce9e8076e491d..4819a54c569bda47b6b6c45bad477c774db277db 100644 --- a/gso/products/product_blocks/iptrunk.py +++ b/gso/products/product_blocks/iptrunk.py @@ -27,15 +27,18 @@ class PhyPortCapacity(strEnum): class IptrunkType(strEnum): + """Types of IP trunks. Can be dark fiber or a leased line.""" + DARK_FIBER = "Dark_fiber" LEASED = "Leased" -T = TypeVar("T", covariant=True) +T_co = TypeVar("T_co", covariant=True) + +class LAGMemberList(UniqueConstrainedList[T_co]): + """A list of :term:`LAG` member interfaces.""" -class LAGMemberList(UniqueConstrainedList[T]): # type: ignore[type-var] - pass class IptrunkInterfaceBlockInactive( @@ -43,22 +46,30 @@ class IptrunkInterfaceBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkInterfaceBlock", ): + """An inactive IP trunk interface.""" + # TODO: add validation for interface names, making the type a constrained string interface_name: str | None = None interface_description: str | None = None class IptrunkInterfaceBlockProvisioning(IptrunkInterfaceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An IP trunk interface that is being provisioned.""" + interface_name: str interface_description: str class IptrunkInterfaceBlock(IptrunkInterfaceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active IP trunk interface.""" + interface_name: str interface_description: str -class IptrunkSides(UniqueConstrainedList[T]): # type: ignore[type-var] +class IptrunkSides(UniqueConstrainedList[T_co]): + """A list of IP trunk interfaces that make up one side of a link.""" + min_items = 2 max_items = 2 @@ -68,6 +79,8 @@ class IptrunkSideBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkSideBlock", ): + """An inactive IP trunk side.""" + iptrunk_side_node: RouterBlockInactive iptrunk_side_ae_iface: str | None = None iptrunk_side_ae_geant_a_sid: str | None = None @@ -75,6 +88,8 @@ class IptrunkSideBlockInactive( class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An IP trunk side that is being provisioned.""" + iptrunk_side_node: RouterBlockProvisioning iptrunk_side_ae_iface: str | None = None iptrunk_side_ae_geant_a_sid: str | None = None @@ -82,6 +97,8 @@ class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[Subscrip class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active IP trunk side.""" + iptrunk_side_node: RouterBlock iptrunk_side_ae_iface: str | None = None iptrunk_side_ae_geant_a_sid: str | None = None diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py index 80985885e807621e4751fcd94d0601e786572524..d902a8fecebf43111cb539ef54775ec591f60fda 100644 --- a/gso/products/product_blocks/router.py +++ b/gso/products/product_blocks/router.py @@ -60,6 +60,7 @@ class RouterBlockInactive( def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: + """Generate an :term:`FQDN` from a hostname, site name, and a country code.""" return f"{hostname}.{site_name.lower()}.{country_code.lower()}.geant.net" diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py index 7da15a8604941c582d92b3bc958f1e1e417fdc91..140e065b37a7f23cf25525c6a8c0d1234443aacf 100644 --- a/gso/services/netbox_client.py +++ b/gso/services/netbox_client.py @@ -127,9 +127,7 @@ class NetboxClient: """Create a new device type in Netbox.""" # First get manufacturer id manufacturer_id = int(self.netbox.dcim.manufacturers.get(name=manufacturer).id) - device_type = DeviceType( - **{"manufacturer": manufacturer_id, "model": model, "slug": slug}, # type: ignore[arg-type] - ) + device_type = DeviceType(manufacturer=manufacturer_id, model=model, slug=slug) return self.netbox.dcim.device_types.create(dict(device_type)) def create_device_role(self, name: str, slug: str) -> DeviceRole: diff --git a/pyproject.toml b/pyproject.toml index e2496cb81797a13eb07f7d142d22dc36477b9abb..943f74b37a8547d7026f5e3f157766b73aad73ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,6 @@ select = [ "F", "FA", "FBT", - "FIX", "FLY", "FURB", "G", diff --git a/utils/netboxcli.py b/utils/netboxcli.py index f5bdc92b598a41b3c1c921481d7914d7a79c7a4f..17e496bd275a6586110a6d2f3b44d44b63e67f54 100644 --- a/utils/netboxcli.py +++ b/utils/netboxcli.py @@ -8,6 +8,7 @@ from gso.services.netbox_client import NetboxClient def convert_to_table(data: list[dict[str, Any]], fields: list[str]) -> pd.DataFrame: + """Convert raw data into a Pandas data table.""" if not data: msg = "No data is available for your request" raise ValueError(msg) @@ -33,6 +34,7 @@ def create() -> None: @click.option("--fqdn", prompt="Enter device name", help="Device name") @click.option("--model", default="vmx", help="Device model") def device(fqdn: str, model: str) -> None: + """Create a new device in Netbox.""" click.echo(f"Creating device: fqdn={fqdn}, model={model}") new_device = NetboxClient().create_device(fqdn, model) click.echo(new_device) @@ -44,6 +46,7 @@ def device(fqdn: str, model: str) -> None: @click.option("--speed", default="1000", help="Interface speed , default is 1000") @click.option("--fqdn", help="Device where to create interface") def interface(name: str, type: str, speed: str, fqdn: str) -> None: + """Create a new interface in Netbox.""" click.echo(f"Creating interface: name={name}, speed={speed}, fqdn={fqdn}") new_interface = NetboxClient().create_interface(name, type, speed, fqdn) click.echo(new_interface) @@ -53,6 +56,7 @@ def interface(name: str, type: str, speed: str, fqdn: str) -> None: @click.option("--name", help="Manufacturer name") @click.option("--slug", help="Short name for manufacturer") def manufacturer(name: str, slug: str) -> None: + """Add a new manufacturer to Netbox.""" click.echo(f"Creating manufacturer: name={name}") manufacturer = NetboxClient().create_device_manufacturer(name, slug) click.echo(manufacturer) @@ -63,6 +67,7 @@ def manufacturer(name: str, slug: str) -> None: @click.option("--model", help="Model for device") @click.option("--slug", help="Short name for manufacturer") def device_type(manufacturer: str, model: str, slug: str) -> None: + """Create a new device type in Netbox.""" click.echo(f"Creating device type: manufacturer={manufacturer} model = {model}") device_type = NetboxClient().create_device_type(manufacturer, model, slug) click.echo(device_type) @@ -72,6 +77,7 @@ def device_type(manufacturer: str, model: str, slug: str) -> None: @click.option("--name", help="Name for device role") @click.option("--slug", help="Short name for device role") def device_role(name: str, slug: str) -> None: + """Create a new device role in Netbox.""" click.echo(f"Creating device role: name={name}") device_role = NetboxClient().create_device_role(name, slug) click.echo(device_role) @@ -81,6 +87,7 @@ def device_role(name: str, slug: str) -> None: @click.option("--name", help="Name for device site") @click.option("--slug", help="Short name for device site") def device_site(name: str, slug: str) -> None: + """Create a new device site in Netbox.""" click.echo(f"Creating device site: name={name}") device_site = NetboxClient().create_device_site(name, slug) click.echo(device_site) @@ -97,7 +104,7 @@ create.add_command(device_site) # Define list commands here @cli.group() def list() -> None: - pass + """Definitions of all listing commands.""" @list.command() @@ -108,6 +115,7 @@ def list() -> None: help="Interface speed to list interfaces (default 1000=1G)", ) def interfaces(fqdn: str, speed: str) -> None: + """List all interfaces that belong to a given :term:`FQDN`.""" click.echo(f"Listing all interfaces for: device with fqdn={fqdn}, speed={speed}") interface_list = NetboxClient().get_interfaces_by_device(fqdn, speed) display_fields = [ @@ -128,12 +136,11 @@ def interfaces(fqdn: str, speed: str) -> None: @list.command() def devices() -> None: + """List all devices in Netbox.""" click.echo("Listing all devices:") device_list = NetboxClient().get_all_devices() display_fields = ["name", "device_type"] - devices = [] - for device in device_list: - devices.append(dict(device)) + devices = [dict(device) for device in device_list] table = convert_to_table(devices, display_fields) click.echo(table) @@ -146,12 +153,13 @@ list.add_command(devices) # Define delete commands here @cli.group() def delete() -> None: - pass + """Definitions of delete commands.""" @delete.command() # type: ignore[no-redef] @click.option("--fqdn", help="Name of device to delete") def device(fqdn: str) -> None: + """Delete a device from Netbox.""" click.echo(f"Deleting device: device={fqdn}") NetboxClient().delete_device(fqdn) @@ -160,6 +168,7 @@ def device(fqdn: str) -> None: @click.option("--fqdn", help="Device name from where to get interface to delete") @click.option("--iface", help="Name of interface name to delete") def interface(fqdn: str, iface: str) -> None: + """Delete an interface from Netbox.""" click.echo(f"Deleting interface: device={fqdn}, interface name={iface}") NetboxClient().delete_interface(fqdn, iface) @@ -171,13 +180,14 @@ delete.add_command(interface) # The action command @cli.group() def action() -> None: - pass + """Available actions.""" @action.command() @click.option("--fqdn", help="Device name from where to get interface to edit") @click.option("--iface", help="Interface name to edit") def reserve_interface(fqdn: str, iface: str) -> None: + """Reserve an available interface in Netbox.""" click.echo(f"Reserving interface: device ={fqdn}, interface name={iface}") reserved_iface = NetboxClient().reserve_interface(fqdn, iface) click.echo(reserved_iface) @@ -187,6 +197,7 @@ def reserve_interface(fqdn: str, iface: str) -> None: @click.option("--fqdn", help="Device name from where to get interface to edit") @click.option("--iface", help="Interface name to edit") def free_interface(fqdn: str, iface: str) -> None: + """Mark a taken interface in Netbox as free.""" click.echo(f"Freeing interface: device={fqdn}, interface name={iface}") freed_iface = NetboxClient().free_interface(fqdn, iface) click.echo(freed_iface) @@ -196,6 +207,7 @@ def free_interface(fqdn: str, iface: str) -> None: @click.option("--fqdn", help="Device name from where to get interface to edit") @click.option("--iface", help="Interface name to edit") def allocate_interface(fqdn: str, iface: str) -> None: + """Allocate a new interface in Netbox.""" click.echo(f"Allocating interface: device={fqdn}, interface name={iface}") allocated_iface = NetboxClient().allocate_interface(fqdn, iface) click.echo(allocated_iface) @@ -205,6 +217,7 @@ def allocate_interface(fqdn: str, iface: str) -> None: @click.option("--fqdn", help="Device name from where to get interface to edit") @click.option("--iface", help="Interface name to edit") def deallocate_interface(fqdn: str, iface: str) -> None: + """Deallocate an existing interface in Netbox.""" click.echo(f"Deallocating interface: device={fqdn}, interface name={iface}") deallocated_iface = NetboxClient().free_interface(fqdn, iface) click.echo(deallocated_iface) @@ -215,6 +228,7 @@ def deallocate_interface(fqdn: str, iface: str) -> None: @click.option("--lag", help="LAG name to attach physical interface to") @click.option("--iface", help="Interface name to attach to LAG") def attach_interface_to_lag(fqdn: str, lag: str, iface: str) -> None: + """Attach an interface to a :term:`LAG`.""" click.echo(f"Attaching LAG to physical interface: device={fqdn}, LAG name={lag}, interface name={iface}") attached_iface = NetboxClient().attach_interface_to_lag(fqdn, lag, iface) click.echo(attached_iface) @@ -225,6 +239,7 @@ def attach_interface_to_lag(fqdn: str, lag: str, iface: str) -> None: @click.option("--lag", help="LAG name to detach from physical interface") @click.option("--iface", help="Interface name to detach LAG from") def detach_interface_from_lag(fqdn: str, lag: str, iface: str) -> None: + """Detach an interface from a :term:`LAG`.""" click.echo(f"Detaching LAG from physical interface: device={fqdn}, LAG name={lag}, interface name={iface}") NetboxClient().detach_interfaces_from_lag(fqdn, lag) click.echo(f"Detached LAG from physical interface: device={fqdn}, LAG name={lag}, interface name={iface}")