Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • goat/gap/geant-service-orchestrator
1 result
Show changes
Commits on Source (9)
Showing
with 334 additions and 45 deletions
......@@ -3,6 +3,9 @@ Glossary of terms
.. glossary::
AAI
Authentication and Authorisation Infrastructure
API
Application Programming Interface
......@@ -23,9 +26,15 @@ Glossary of terms
CRUD
Create, Read, Update, Delete
DNS
Domain Name System
FQDN
Fully Quantified Domain Name
GAP
The GÉANT Automation Platform
GSO
GÉANT Service Orchestrator
......@@ -42,6 +51,9 @@ Glossary of terms
ISO
International Organisation for Standardisation
JSON
JavaScript Object Notation
LAG
Link Aggregation: a bundle of multiple network connections.
......@@ -69,6 +81,3 @@ Glossary of terms
WFO
`Workflow Orchestrator <https://workfloworchestrator.org/>`_
AAI
Authentication and Authorisation Infrastructure
``gso.workflows.router.modify_connection_strategy``
=========================================
.. automodule:: gso.workflows.router.modify_connection_strategy
:members:
:show-inheritance:
GÉANT Automation Platform
G[ÉE]ANT
[GSO|gso]
(GSO|gso)
N(okia|OKIA)
IMS
Vereniging
[[T|t]erminate|TERMINATE]
[T|t]erminate
TERMINATED?
WFO
Ansible
[Dd]eprovision
......@@ -9,12 +14,15 @@ API
DNS
dry_run
Dark_fiber
[A|a]ddress
[I|i]ptrunk
[A|a]llocate
[Aa]ddress
[Ii]ptrunk
[Aa]llocate
PHASE 1
[Mm]odify
FQDN
AAI
[M|m]iddleware
[Mm]iddleware
TWAMP
Pydantic
UUID
SNMP
......@@ -2,14 +2,16 @@
from typing import Any
from fastapi import Depends, status
from fastapi import Depends, Response, status
from fastapi.routing import APIRouter
from orchestrator.domain import SubscriptionModel
from orchestrator.schemas import SubscriptionDomainModelSchema
from orchestrator.services.subscriptions import build_extended_domain_model
from orchestrator.types import SubscriptionLifecycle
from gso.auth.api_key_auth import get_api_key
from gso.services.subscriptions import get_active_router_subscriptions
from gso.products import ProductType
from gso.services.subscriptions import get_router_subscriptions, get_subscriptions
router = APIRouter(
prefix="/subscriptions",
......@@ -24,11 +26,44 @@ router = APIRouter(
response_model=list[SubscriptionDomainModelSchema],
)
def subscription_routers() -> list[dict[str, Any]]:
"""Retrieve all active router subscriptions."""
"""Retrieve all active or provisioning router subscriptions."""
subscriptions = []
for r in get_active_router_subscriptions():
routers = get_router_subscriptions(lifecycles=[SubscriptionLifecycle.ACTIVE, SubscriptionLifecycle.PROVISIONING])
for r in routers:
subscription = SubscriptionModel.from_subscription(r["subscription_id"])
extended_model = build_extended_domain_model(subscription)
subscriptions.append(extended_model)
return subscriptions
@router.get(
"/dashboard_devices",
status_code=status.HTTP_200_OK,
response_class=Response,
responses={
200: {
"content": {"text/plain": {}},
"description": "Return a flat file of FQDNs.",
}
},
)
def subscription_dashboard_devices() -> Response:
"""Retrieve FQDN for all dashboard devices that are monitored."""
fqdns = []
dashboard_devices = get_subscriptions(
product_types=[ProductType.ROUTER, ProductType.SUPER_POP_SWITCH, ProductType.OFFICE_ROUTER],
lifecycles=[SubscriptionLifecycle.ACTIVE, SubscriptionLifecycle.PROVISIONING],
)
for device in dashboard_devices:
subscription = SubscriptionModel.from_subscription(device["subscription_id"])
extended_model = build_extended_domain_model(subscription)
if extended_model["product"]["product_type"] == ProductType.ROUTER:
fqdns.append(extended_model["router"]["router_fqdn"])
elif extended_model["product"]["product_type"] == ProductType.SUPER_POP_SWITCH:
fqdns.append(extended_model["super_pop_switch"]["super_pop_switch_fqdn"])
elif extended_model["product"]["product_type"] == ProductType.OFFICE_ROUTER:
fqdns.append(extended_model["office_router"]["office_router_fqdn"])
fqdn_flat_file = "\n".join(fqdns)
return Response(content=fqdn_flat_file, media_type="text/plain")
......@@ -11,7 +11,7 @@ from alembic import op
# revision identifiers, used by Alembic.
revision = 'e8378fbcfbf3'
down_revision = 'da5c9f4cce1c'
branch_labels = ('data',)
branch_labels = None
depends_on = None
......
"""Add subscription cancellation workflow.
Revision ID: 734e36a3e70b
Revises: d61c0f92da1e
Create Date: 2024-03-21 13:03:08.981028
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '734e36a3e70b'
down_revision = 'a2cd3f2e6d7a'
branch_labels = None
depends_on = None
from orchestrator.migrations.helpers import add_products_to_workflow_by_product_tag, create_workflow, delete_workflow, remove_products_from_workflow_by_product_tag
products = ["RTR", "IPTRUNK"]
new_workflows = [
{
"name": "cancel_subscription",
"target": "TERMINATE",
"description": "Cancel a subscription",
"product_type": "Site"
}
]
def upgrade() -> None:
conn = op.get_bind()
for workflow in new_workflows:
create_workflow(conn, workflow)
for product_tag in products:
add_products_to_workflow_by_product_tag(conn, "cancel_subscription", product_tag)
def downgrade() -> None:
conn = op.get_bind()
for product_tag in products:
remove_products_from_workflow_by_product_tag(conn, "cancel_subscription", product_tag)
for workflow in new_workflows:
delete_workflow(conn, workflow["name"])
"""Modify connection streategy workflow..
Revision ID: a2cd3f2e6d7a
Revises:
Create Date: 2024-03-21 16:05:59.043106
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'a2cd3f2e6d7a'
down_revision = None
branch_labels = None
depends_on = 'd61c0f92da1e'
from orchestrator.migrations.helpers import create_workflow, delete_workflow
new_workflows = [
{
"name": "modify_connection_strategy",
"target": "MODIFY",
"description": "Modify connection strategy",
"product_type": "Router"
}
]
def upgrade() -> None:
conn = op.get_bind()
for workflow in new_workflows:
create_workflow(conn, workflow)
def downgrade() -> None:
conn = op.get_bind()
for workflow in new_workflows:
delete_workflow(conn, workflow["name"])
......@@ -15,8 +15,8 @@ from gso.products.product_types.site import Site
from gso.products.product_types.super_pop_switch import SuperPopSwitch
class ProductType(strEnum):
"""An enumerator of available products in :term:`GSO`."""
class ProductName(strEnum):
"""An enumerator of available product names in :term:`GSO`."""
IP_TRUNK = "IP trunk"
ROUTER = "Router"
......@@ -25,12 +25,22 @@ class ProductType(strEnum):
OFFICE_ROUTER = "Office router"
class ProductType(strEnum):
"""An enumerator of available product types in :term:`GSO`."""
IP_TRUNK = Iptrunk.__name__
ROUTER = Router.__name__
SITE = Site.__name__
SUPER_POP_SWITCH = SuperPopSwitch.__name__
OFFICE_ROUTER = OfficeRouter.__name__
SUBSCRIPTION_MODEL_REGISTRY.update(
{
"IP trunk": Iptrunk,
"Router": Router,
"Site": Site,
"Super PoP switch": SuperPopSwitch,
"Office router": OfficeRouter,
ProductName.IP_TRUNK.value: Iptrunk,
ProductName.ROUTER.value: Router,
ProductName.SITE.value: Site,
ProductName.SUPER_POP_SWITCH.value: SuperPopSwitch,
ProductName.OFFICE_ROUTER.value: OfficeRouter,
},
)
......@@ -19,28 +19,31 @@ from orchestrator.services.subscriptions import query_in_use_by_subscriptions
from orchestrator.types import SubscriptionLifecycle
from pydantic_forms.types import UUIDstr
from gso.products import ProductType
from gso.products import ProductName, ProductType
from gso.products.product_types.site import Site
SubscriptionType = dict[str, Any]
def get_subscriptions(
product_type: str,
lifecycle: SubscriptionLifecycle,
product_types: list[ProductType],
lifecycles: list[SubscriptionLifecycle] | None = None,
includes: list[str] | None = None,
excludes: list[str] | None = None,
) -> list[SubscriptionType]:
"""Retrieve active subscriptions for a specific product type.
:param str product_type: The type of the product for which to retrieve subscriptions.
:param SubscriptionLifecycle lifecycle: The lifecycle that the products must be in.
:param list[ProductName] product_types: The types of the product for which to retrieve subscriptions.
:param SubscriptionLifecycle lifecycles: The lifecycles that the products must be in.
:param list[str] includes: List of fields to be included in the returned Subscription objects.
:param list[str] excludes: List of fields to be excluded from the returned Subscription objects.
:return: A list of Subscription objects that match the query.
:rtype: list[Subscription]
"""
if not lifecycles:
lifecycles = list(SubscriptionLifecycle)
if not includes:
includes = [col.name for col in SubscriptionTable.__table__.columns]
......@@ -50,8 +53,8 @@ def get_subscriptions(
dynamic_fields = [getattr(SubscriptionTable, field) for field in includes]
query = SubscriptionTable.query.join(ProductTable).filter(
ProductTable.product_type == product_type,
SubscriptionTable.status == lifecycle,
ProductTable.product_type.in_([str(product_type) for product_type in product_types]),
SubscriptionTable.status.in_([str(lifecycle) for lifecycle in lifecycles]),
)
results = query.with_entities(*dynamic_fields).all()
......@@ -68,7 +71,23 @@ def get_active_site_subscriptions(includes: list[str] | None = None) -> list[Sub
:return: A list of Subscription objects for sites.
:rtype: list[Subscription]
"""
return get_subscriptions(product_type=ProductType.SITE, lifecycle=SubscriptionLifecycle.ACTIVE, includes=includes)
return get_subscriptions(
product_types=[ProductType.SITE], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes
)
def get_router_subscriptions(
includes: list[str] | None = None, lifecycles: list[SubscriptionLifecycle] | None = None
) -> list[SubscriptionType]:
"""Retrieve subscriptions specifically for routers.
:param includes: The fields to be included in the returned Subscription objects.
:type includes: list[str]
:return: A list of Subscription objects for routers.
:rtype: list[Subscription]
"""
return get_subscriptions(product_types=[ProductType.ROUTER], lifecycles=lifecycles, includes=includes)
def get_active_router_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]:
......@@ -80,7 +99,9 @@ def get_active_router_subscriptions(includes: list[str] | None = None) -> list[S
:return: A list of Subscription objects for routers.
:rtype: list[Subscription]
"""
return get_subscriptions(product_type="Router", lifecycle=SubscriptionLifecycle.ACTIVE, includes=includes)
return get_subscriptions(
product_types=[ProductType.ROUTER], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes
)
def get_provisioning_router_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]:
......@@ -89,7 +110,9 @@ def get_provisioning_router_subscriptions(includes: list[str] | None = None) ->
:param list[str] includes: The fields to be included in the returned Subscription objects.
:return list[Subscription]: A list of router Subscription objects.
"""
return get_subscriptions(product_type="Router", lifecycle=SubscriptionLifecycle.PROVISIONING, includes=includes)
return get_subscriptions(
product_types=[ProductType.ROUTER], lifecycles=[SubscriptionLifecycle.PROVISIONING], includes=includes
)
def get_active_iptrunk_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]:
......@@ -101,7 +124,9 @@ def get_active_iptrunk_subscriptions(includes: list[str] | None = None) -> list[
:return: A list of Subscription objects for IP trunks.
:rtype: list[Subscription]
"""
return get_subscriptions(product_type="Iptrunk", lifecycle=SubscriptionLifecycle.ACTIVE, includes=includes)
return get_subscriptions(
product_types=[ProductType.IP_TRUNK], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes
)
def get_active_trunks_that_terminate_on_router(subscription_id: UUIDstr) -> list[SubscriptionTable]:
......@@ -120,18 +145,18 @@ def get_active_trunks_that_terminate_on_router(subscription_id: UUIDstr) -> list
query_in_use_by_subscriptions(UUID(subscription_id))
.join(ProductTable)
.filter(
ProductTable.product_type == "Iptrunk",
SubscriptionTable.status == "active",
ProductTable.product_type == ProductType.IP_TRUNK,
SubscriptionTable.status == SubscriptionLifecycle.ACTIVE,
)
.all()
)
def get_product_id_by_name(product_name: ProductType) -> UUID:
def get_product_id_by_name(product_name: ProductName) -> UUID:
"""Retrieve the :term:`UUID` of a product by its name.
:param product_name: The name of the product.
:type product_name: ProductType
:type product_name: ProductName
:return UUID: The :term:`UUID` of the product.
:rtype: UUID
......
......@@ -38,10 +38,12 @@
"workflow": {
"activate_iptrunk": "Activate IP Trunk",
"activate_router": "Activate router",
"cancel_subscription": "Cancel subscription",
"confirm_info": "Please verify this form looks correct.",
"deploy_twamp": "Deploy TWAMP",
"migrate_iptrunk": "Migrate IP Trunk",
"modify_isis_metric": "Modify the ISIS metric",
"modify_site": "Modify site",
"modify_trunk_interface": "Modify IP Trunk interface",
"redeploy_base_config": "Redeploy base config",
"update_ibgp_mesh": "Update iBGP mesh"
......
......@@ -19,3 +19,10 @@ class PortNumber(ConstrainedInt):
gt = 0
le = 49151
class ConnectionStrategy(strEnum):
"""An enumerator for the connection Strategies."""
IN_BAND = "IN BAND"
OUT_OF_BAND = "OUT OF BAND"
......@@ -5,6 +5,7 @@ from orchestrator.workflows import LazyWorkflowInstance
WF_USABLE_MAP.update(
{
"cancel_subscription": ["initial"],
"redeploy_base_config": ["provisioning", "active"],
"update_ibgp_mesh": ["provisioning", "active"],
"activate_router": ["provisioning"],
......@@ -26,6 +27,8 @@ LazyWorkflowInstance("gso.workflows.router.create_router", "create_router")
LazyWorkflowInstance("gso.workflows.router.redeploy_base_config", "redeploy_base_config")
LazyWorkflowInstance("gso.workflows.router.terminate_router", "terminate_router")
LazyWorkflowInstance("gso.workflows.router.update_ibgp_mesh", "update_ibgp_mesh")
LazyWorkflowInstance("gso.workflows.router.modify_connection_strategy", "modify_connection_strategy")
LazyWorkflowInstance("gso.workflows.shared.cancel_subscription", "cancel_subscription")
LazyWorkflowInstance("gso.workflows.site.create_site", "create_site")
LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_site")
LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site")
......
"""Modify connection strategy workflow. Flipping the connection between in-band to out-of-band."""
from orchestrator.forms import FormPage
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_types.router import Router
from gso.utils.shared_enums import ConnectionStrategy
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
"""Modify the connection strategy initial formruff format."""
subscription = Router.from_subscription(subscription_id)
current_connection_strategy = (
ConnectionStrategy.OUT_OF_BAND if subscription.router.router_access_via_ts else ConnectionStrategy.IN_BAND
)
class ModifyConnectionStrategyForm(FormPage):
class Config:
title = f"Modify the connection strategy of {subscription.router.router_fqdn}."
connection_strategy: ConnectionStrategy = current_connection_strategy
user_input = yield ModifyConnectionStrategyForm
return user_input.dict()
@step("Update subscription model")
def update_subscription_model(subscription: Router, connection_strategy: str) -> State:
"""Update the database model to reflect the new connection strategy.
If the connection strategy is set to IN-BAND, then access_via_ts should be set to False.
Conversely, if the connection strategy is set to OUT-OF-BAND, access_via_ts should be set to True.
"""
subscription.router.router_access_via_ts = connection_strategy == ConnectionStrategy.OUT_OF_BAND
return {"subscription": subscription}
@workflow(
"Modify connection strategy",
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
target=Target.MODIFY,
)
def modify_connection_strategy() -> StepList:
"""Modify the connection strategy."""
return init >> store_process_subscription(Target.MODIFY) >> unsync >> update_subscription_model >> resync >> done
"""Workflows that are shared across multiple products."""
"""Cancel a subscription that is in the initial lifecycle state."""
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Label
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator:
class CancelSubscriptionForm(FormPage):
info_label: Label = f"Canceling subscription with ID {subscription_id}" # type:ignore[assignment]
info_label_2: Label = (
"This will immediately mark the subscription as terminated, preventing any other workflows from interacting" # type:ignore[assignment]
" with this product subscription."
)
info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." # type:ignore[assignment]
info_label_4: Label = "THIS WORKFLOW IS IRREVERSIBLE AND MAY HAVE UNFORESEEN CONSEQUENCES." # type:ignore[assignment]
yield CancelSubscriptionForm
return {"subscription_id": subscription_id}
@workflow(
"Cancel an initial subscription",
initial_input_form=wrap_modify_initial_input_form(_initial_input_form),
target=Target.TERMINATE,
)
def cancel_subscription() -> StepList:
"""Cancel an initial subscription, taking it from the ``INITIAL`` state straight to ``TERMINATED``.
This workflow can be used when a creation workflow has failed, and the process needs to be restarted. This workflow
will prevent a stray subscription, forever stuck in the initial state, to stick around.
* Update the subscription lifecycle state to ``TERMINATED``.
"""
return (
init
>> store_process_subscription(Target.TERMINATE)
>> unsync
>> set_status(SubscriptionLifecycle.TERMINATED)
>> resync
>> done
)
......@@ -11,7 +11,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from gso.products import ProductType
from gso.products import ProductName
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhyPortCapacity
from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
from gso.products.product_types.router import Router
......@@ -68,7 +68,7 @@ def initial_input_form_generator() -> FormGenerator:
def create_subscription(partner: str) -> State:
"""Create a new subscription in the service database."""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id = subscriptions.get_product_id_by_name(ProductType.IP_TRUNK)
product_id = subscriptions.get_product_id_by_name(ProductName.IP_TRUNK)
subscription = IptrunkInactive.from_product_id(product_id, partner_id)
return {
......
......@@ -9,7 +9,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from gso.products import ProductType
from gso.products import ProductName
from gso.products.product_types import office_router
from gso.products.product_types.office_router import OfficeRouterInactive
from gso.services import subscriptions
......@@ -22,7 +22,7 @@ from gso.utils.shared_enums import PortNumber, Vendor
def create_subscription(partner: str) -> State:
"""Create a new subscription object."""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id = subscriptions.get_product_id_by_name(ProductType.OFFICE_ROUTER)
product_id = subscriptions.get_product_id_by_name(ProductName.OFFICE_ROUTER)
subscription = OfficeRouterInactive.from_product_id(product_id, partner_id)
return {
......
......@@ -9,7 +9,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from gso.products import ProductType
from gso.products import ProductName
from gso.products.product_blocks import router as router_pb
from gso.products.product_blocks.router import RouterRole
from gso.products.product_types import router
......@@ -25,7 +25,7 @@ from gso.utils.shared_enums import PortNumber, Vendor
def create_subscription(partner: str) -> State:
"""Create a new subscription object."""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id = subscriptions.get_product_id_by_name(ProductType.ROUTER)
product_id = subscriptions.get_product_id_by_name(ProductName.ROUTER)
subscription = RouterInactive.from_product_id(product_id, partner_id)
return {
......
......@@ -8,7 +8,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from gso.products import ProductType
from gso.products import ProductName
from gso.products.product_blocks.site import SiteTier
from gso.products.product_types.site import SiteInactive
from gso.services import subscriptions
......@@ -23,7 +23,7 @@ def create_subscription(partner: str) -> State:
FIXME: all attributes passed by the input form appear to be unused
"""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id: UUID = subscriptions.get_product_id_by_name(ProductType.SITE)
product_id: UUID = subscriptions.get_product_id_by_name(ProductName.SITE)
subscription = SiteInactive.from_product_id(product_id, partner_id)
return {
......
......@@ -9,7 +9,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from gso.products import ProductType
from gso.products import ProductName
from gso.products.product_types import super_pop_switch
from gso.products.product_types.super_pop_switch import SuperPopSwitchInactive
from gso.services import subscriptions
......@@ -23,7 +23,7 @@ from gso.utils.shared_enums import PortNumber, Vendor
def create_subscription(partner: str) -> State:
"""Create a new subscription object."""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id = subscriptions.get_product_id_by_name(ProductType.SUPER_POP_SWITCH)
product_id = subscriptions.get_product_id_by_name(ProductName.SUPER_POP_SWITCH)
subscription = SuperPopSwitchInactive.from_product_id(product_id, partner_id)
return {
......