Skip to content
Snippets Groups Projects
Verified Commit 3f47469e authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

revert changes to the router model

vendor is moved back into the router product block
parent a4ef724d
No related branches found
No related tags found
1 merge request!120revert changes to the router model
Pipeline #84674 passed
Showing
with 139 additions and 58 deletions
......@@ -12,9 +12,8 @@ from pydantic import BaseModel, root_validator, validator
from pydantic.fields import ModelField
from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
from gso.products.product_blocks.router import RouterRole
from gso.products.product_blocks.router import RouterRole, RouterVendor
from gso.products.product_blocks.site import SiteTier
from gso.products.product_types.router import RouterVendor
from gso.services import subscriptions
from gso.services.crm import CustomerNotFoundError, get_customer_by_name
from gso.utils.helpers import (
......
......@@ -10,7 +10,7 @@ from alembic import op
# revision identifiers, used by Alembic.
revision = '6dbd6c4c04b4'
down_revision = '815033570ad7'
down_revision = '0c31b60487c8'
branch_labels = None
depends_on = None
......
......@@ -10,7 +10,7 @@ from alembic import op
# revision identifiers, used by Alembic.
revision = '815033570ad7'
down_revision = '0c31b60487c8'
down_revision = '0c904cf0b66b'
branch_labels = None
depends_on = None
......
"""Update router model - vendor from type to block.
Revision ID: 0c904cf0b66b
Revises: 6dbd6c4c04b4
Create Date: 2023-11-24 16:14:48.552842
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '0c904cf0b66b'
down_revision = '6dbd6c4c04b4'
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Nokia router', 'Juniper router'))))
"""))
conn.execute(sa.text("""
DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Nokia router', 'Juniper router')))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Nokia router', 'Juniper router')))
"""))
conn.execute(sa.text("""
DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Nokia router', 'Juniper router'))
"""))
conn.execute(sa.text("""
DELETE FROM products WHERE products.name IN ('Nokia router', 'Juniper router')
"""))
conn.execute(sa.text("""
INSERT INTO products (name, description, product_type, tag, status) VALUES ('Router', 'A Router', 'Router', 'RTR', 'active') RETURNING products.product_id
"""))
conn.execute(sa.text("""
INSERT INTO resource_types (resource_type, description) VALUES ('vendor', 'Router vendor') RETURNING resource_types.resource_type_id
"""))
conn.execute(sa.text("""
INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Router')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock')))
"""))
conn.execute(sa.text("""
INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor')))
"""))
def downgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor'))
"""))
conn.execute(sa.text("""
DELETE FROM resource_types WHERE resource_types.resource_type IN ('vendor')
"""))
conn.execute(sa.text("""
DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Router')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock'))
"""))
conn.execute(sa.text("""
DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Router'))))
"""))
conn.execute(sa.text("""
DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Router')))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Router')))
"""))
conn.execute(sa.text("""
DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Router'))
"""))
conn.execute(sa.text("""
DELETE FROM products WHERE products.name IN ('Router')
"""))
......@@ -16,18 +16,15 @@ from gso.products.product_types.site import Site
class ProductType(strEnum):
"""An enumerator of available products in :term:`GSO`."""
SITE = "Site"
NOKIA_ROUTER = "Nokia router"
JUNIPER_ROUTER = "Juniper router"
IP_TRUNK = "IP trunk"
ROUTER = "Router"
SITE = "Site"
SUBSCRIPTION_MODEL_REGISTRY.update(
{
"IP trunk": Iptrunk,
"Juniper router": Router,
"Nokia router": Router,
"Router": Router,
"Site": Site,
},
)
......@@ -21,6 +21,13 @@ class RouterRole(strEnum):
AMT = "amt"
class RouterVendor(strEnum):
"""Enumerator for the different product vendors that are supported."""
JUNIPER = "juniper"
NOKIA = "nokia"
class PortNumber(ConstrainedInt):
"""Constrained integer for valid port numbers.
......@@ -46,6 +53,7 @@ class RouterBlockInactive(
router_lo_iso_address: str | None = None
router_role: RouterRole | None = None
router_site: SiteBlockInactive | None
vendor: RouterVendor | None = None
def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str:
......@@ -64,6 +72,7 @@ class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecy
router_lo_iso_address: str | None = None
router_role: RouterRole | None = None
router_site: SiteBlockProvisioning | None
vendor: RouterVendor
class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
......@@ -85,3 +94,5 @@ class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTI
router_role: RouterRole
#: The :class:`Site` that this router resides in. Both physically and computationally.
router_site: SiteBlock
#: The vendor of a router.
vendor: RouterVendor
......@@ -2,7 +2,6 @@
from orchestrator.domain.base import SubscriptionModel
from orchestrator.types import SubscriptionLifecycle
from pydantic_forms.types import strEnum
from gso.products.product_blocks.router import (
RouterBlock,
......@@ -11,29 +10,19 @@ from gso.products.product_blocks.router import (
)
class RouterVendor(strEnum):
"""Enumerator for the different product vendors that are supported."""
JUNIPER = "juniper"
NOKIA = "nokia"
class RouterInactive(SubscriptionModel, is_base=True):
"""An inactive router."""
vendor: RouterVendor
router: RouterBlockInactive
class RouterProvisioning(RouterInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
"""A router that is being provisioned."""
vendor: RouterVendor
router: RouterBlockProvisioning
class Router(RouterProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
"""A router that is currently active."""
vendor: RouterVendor
router: RouterBlock
......@@ -12,8 +12,9 @@ from pydantic import BaseModel
from pydantic_forms.validators import Choice
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router, RouterVendor
from gso.products.product_types.router import Router
from gso.services import provisioning_proxy
from gso.services.netbox_client import NetboxClient
from gso.services.subscriptions import get_active_subscriptions_by_field_and_value
......@@ -122,7 +123,7 @@ def get_router_vendor(router_id: UUID) -> RouterVendor:
:return: The vendor of the router.
:rtype: RouterVendor:
"""
return Router.from_subscription(router_id).vendor
return Router.from_subscription(router_id).router.vendor
def iso_from_ipv4(ipv4_address: IPv4Address) -> str:
......@@ -149,7 +150,7 @@ def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr:
:rtype: :class:`UUIDstr`
"""
router_type = Router.from_subscription(subscription_id)
if router_type.vendor == RouterVendor.NOKIA:
if router_type.router.vendor == RouterVendor.NOKIA:
device = NetboxClient().get_device_by_name(router_type.router.router_fqdn)
if not device:
msg = "The selected router does not exist in Netbox."
......
......@@ -17,8 +17,9 @@ from gso.products.product_blocks.iptrunk import (
IptrunkType,
PhyPortCapacity,
)
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
from gso.products.product_types.router import Router, RouterVendor
from gso.products.product_types.router import Router
from gso.services import infoblox, provisioning_proxy, subscriptions
from gso.services.crm import customer_selector
from gso.services.netbox_client import NetboxClient
......
......@@ -24,8 +24,9 @@ from pydantic_forms.core import ReadOnlyField
from pynetbox.models.dcim import Interfaces
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router, RouterVendor
from gso.products.product_types.router import Router
from gso.services import provisioning_proxy
from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import pp_interaction
......@@ -493,16 +494,15 @@ def reserve_interfaces_in_netbox(
new_lag_member_interfaces: list[dict],
) -> State:
"""Reserve new interfaces in Netbox."""
new_side_type = Router.from_subscription(new_node)
new_side_router = new_side_type.router
new_side = Router.from_subscription(new_node).router
nbclient = NetboxClient()
if new_side_type.vendor == RouterVendor.NOKIA:
if new_side.vendor == RouterVendor.NOKIA:
# Create :term:`LAG` interfaces
lag_interface: Interfaces = nbclient.create_interface(
iface_name=new_lag_interface,
interface_type="lag",
device_name=new_side_router.router_fqdn,
device_name=new_side.router_fqdn,
description=str(subscription.subscription_id),
enabled=True,
)
......@@ -510,13 +510,13 @@ def reserve_interfaces_in_netbox(
# Reserve interfaces
for interface in new_lag_member_interfaces:
nbclient.attach_interface_to_lag(
device_name=new_side_router.router_fqdn,
device_name=new_side.router_fqdn,
lag_name=lag_interface.name,
iface_name=interface["interface_name"],
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=new_side_router.router_fqdn,
device_name=new_side.router_fqdn,
iface_name=interface["interface_name"],
)
return {"subscription": subscription}
......
......@@ -18,8 +18,8 @@ from gso.products.product_blocks.iptrunk import (
IptrunkType,
PhyPortCapacity,
)
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import RouterVendor
from gso.services import provisioning_proxy
from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import pp_interaction
......
......@@ -15,8 +15,8 @@ from orchestrator.workflows.steps import (
)
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import RouterVendor
from gso.services import infoblox, provisioning_proxy
from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import pp_interaction
......
......@@ -15,9 +15,10 @@ from pydantic import validator
from gso.products.product_blocks.router import (
PortNumber,
RouterRole,
RouterVendor,
generate_fqdn,
)
from gso.products.product_types.router import RouterInactive, RouterProvisioning, RouterVendor
from gso.products.product_types.router import RouterInactive, RouterProvisioning
from gso.products.product_types.site import Site
from gso.services import infoblox, provisioning_proxy, subscriptions
from gso.services.crm import customer_selector
......@@ -44,6 +45,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
tt_number: str
customer: customer_selector() # type: ignore[valid-type]
vendor: RouterVendor
router_site: _site_selector() # type: ignore[valid-type]
hostname: str
ts_port: PortNumber
......@@ -87,6 +89,7 @@ def initialize_subscription(
ts_port: PortNumber,
router_site: str,
router_role: RouterRole,
vendor: RouterVendor,
) -> State:
"""Initialise the subscription object in the service database."""
subscription.router.router_ts_port = ts_port
......@@ -99,6 +102,7 @@ def initialize_subscription(
subscription.router.router_fqdn = fqdn
subscription.router.router_role = router_role
subscription.router.router_access_via_ts = True
subscription.router.vendor = vendor
subscription.description = f"Router {fqdn}"
subscription = RouterProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING)
......@@ -151,7 +155,7 @@ def create_netbox_device(subscription: RouterProvisioning) -> State:
HACK: use a conditional instead for execution of this step
"""
if subscription.vendor == RouterVendor.NOKIA:
if subscription.router.vendor == RouterVendor.NOKIA:
NetboxClient().create_device(
subscription.router.router_fqdn,
str(subscription.router.router_site.site_tier), # type: ignore[union-attr]
......
......@@ -16,7 +16,8 @@ from orchestrator.workflows.steps import (
)
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_types.router import Router, RouterVendor
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.router import Router
from gso.services import infoblox
from gso.services.netbox_client import NetboxClient
......@@ -60,7 +61,8 @@ def remove_config_from_router() -> None:
@step("Remove Device from NetBox")
def remove_device_from_netbox(subscription: Router) -> dict[str, Router]:
"""Remove the device from Netbox."""
if subscription.vendor == RouterVendor.NOKIA:
if subscription.router.vendor == RouterVendor.NOKIA:
# TODO: This should be solved with a conditional
NetboxClient().delete_device(subscription.router.router_fqdn)
return {"subscription": subscription}
......
"""A creation workflow that adds an existing router to the service database."""
import ipaddress
from uuid import UUID
from orchestrator import workflow
from orchestrator.forms import FormPage
......@@ -12,9 +11,9 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc
from gso.products import ProductType
from gso.products.product_blocks import router as router_pb
from gso.products.product_blocks.router import PortNumber, RouterRole, generate_fqdn
from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor, generate_fqdn
from gso.products.product_types import router
from gso.products.product_types.router import RouterInactive, RouterVendor
from gso.products.product_types.router import RouterInactive
from gso.products.product_types.site import Site
from gso.services import subscriptions
from gso.services.crm import get_customer_by_name
......@@ -35,15 +34,10 @@ def _get_site_by_name(site_name: str) -> Site:
@step("Create subscription")
def create_subscription(customer: str, router_vendor: RouterVendor) -> State:
def create_subscription(customer: str) -> State:
"""Create a new subscription object."""
customer_id = get_customer_by_name(customer)["id"]
product_id: UUID
if router_vendor == RouterVendor.NOKIA:
product_id = subscriptions.get_product_id_by_name(ProductType.NOKIA_ROUTER)
elif router_vendor == RouterVendor.JUNIPER:
product_id = subscriptions.get_product_id_by_name(ProductType.JUNIPER_ROUTER)
product_id = subscriptions.get_product_id_by_name(ProductType.ROUTER)
subscription = RouterInactive.from_product_id(product_id, customer_id)
return {
......@@ -81,6 +75,7 @@ def initialize_subscription(
ts_port: PortNumber,
router_site: str,
router_role: router_pb.RouterRole,
router_vendor: RouterVendor,
router_lo_ipv4_address: ipaddress.IPv4Address | None = None,
router_lo_ipv6_address: ipaddress.IPv6Address | None = None,
router_lo_iso_address: str | None = None,
......@@ -97,6 +92,7 @@ def initialize_subscription(
subscription.router.router_lo_ipv4_address = router_lo_ipv4_address
subscription.router.router_lo_ipv6_address = router_lo_ipv6_address
subscription.router.router_lo_iso_address = router_lo_iso_address
subscription.router.vendor = router_vendor
subscription = router.RouterProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING)
......
......@@ -12,7 +12,7 @@ from gso.products.product_blocks.iptrunk import (
IptrunkType,
PhyPortCapacity,
)
from gso.products.product_blocks.router import RouterRole
from gso.products.product_blocks.router import RouterRole, RouterVendor
from gso.products.product_blocks.site import SiteTier
from gso.products.product_types.iptrunk import IptrunkInactive
from gso.products.product_types.router import Router, RouterInactive
......@@ -97,7 +97,7 @@ def nokia_router_subscription_factory(site_subscription_factory, faker):
router_lo_iso_address = router_lo_iso_address or faker.word()
router_site = router_site or site_subscription_factory()
product_id = subscriptions.get_product_id_by_name(ProductType.NOKIA_ROUTER)
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.router.router_fqdn = router_fqdn
router_subscription.router.router_ts_port = router_ts_port
......@@ -107,6 +107,7 @@ def nokia_router_subscription_factory(site_subscription_factory, faker):
router_subscription.router.router_lo_iso_address = router_lo_iso_address
router_subscription.router.router_role = router_role
router_subscription.router.router_site = Site.from_subscription(router_site).site
router_subscription.router.vendor = RouterVendor.NOKIA
router_subscription = SubscriptionModel.from_other_lifecycle(router_subscription, SubscriptionLifecycle.ACTIVE)
router_subscription.description = description
......
......@@ -6,9 +6,8 @@ from orchestrator.db import SubscriptionTable
from orchestrator.services import subscriptions
from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
from gso.products.product_blocks.router import RouterRole
from gso.products.product_blocks.router import RouterRole, RouterVendor
from gso.products.product_blocks.site import SiteTier
from gso.products.product_types.router import RouterVendor
from gso.utils.helpers import iso_from_ipv4
SITE_IMPORT_ENDPOINT = "/api/v1/imports/sites"
......
......@@ -3,7 +3,7 @@ from unittest.mock import patch
import pytest
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_types.router import RouterVendor
from gso.products.product_blocks.router import RouterVendor
from gso.utils.helpers import available_interfaces_choices_including_current_members
......@@ -22,20 +22,20 @@ def mock_netbox_client():
def test_non_nokia_router_returns_none(mock_router, faker):
mock_router.from_subscription.return_value.vendor = RouterVendor.JUNIPER
mock_router.from_subscription.return_value.router.vendor = RouterVendor.JUNIPER
result = available_interfaces_choices_including_current_members(faker.uuid4(), "10G", [])
assert result is None
def test_nokia_router_with_no_interfaces_returns_empty_choice(mock_router, mock_netbox_client, faker):
mock_router.from_subscription.return_value.vendor = RouterVendor.NOKIA
mock_router.from_subscription.return_value.router.vendor = RouterVendor.NOKIA
mock_netbox_client().get_available_interfaces.return_value = iter([])
result = available_interfaces_choices_including_current_members(faker.uuid4(), "10G", [])
assert len(result) == 0
def test_nokia_router_with_interfaces_returns_choice(mock_router, mock_netbox_client, faker):
mock_router.from_subscription.return_value.vendor = RouterVendor.NOKIA
mock_router.from_subscription.return_value.router.vendor = RouterVendor.NOKIA
mock_netbox_client().get_available_interfaces.return_value = iter(
[
{"name": "interface1", "module": {"display": "module1"}, "description": "desc1"},
......
......@@ -4,7 +4,7 @@ import pytest
from infoblox_client import objects
from gso.products import ProductType, Site
from gso.products.product_blocks.router import RouterRole
from gso.products.product_blocks.router import RouterRole, RouterVendor
from gso.products.product_types.router import Router
from gso.services.crm import customer_selector, get_customer_by_name
from gso.services.subscriptions import get_product_id_by_name
......@@ -28,6 +28,7 @@ def router_creation_input_form_data(site_subscription_factory, faker):
"hostname": faker.pystr(),
"ts_port": faker.pyint(),
"router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0],
"vendor": RouterVendor.NOKIA,
}
......@@ -48,7 +49,7 @@ def test_create_nokia_router_success(
data_config_filename,
):
# Set up mock return values
product_id = get_product_id_by_name(ProductType.NOKIA_ROUTER)
product_id = get_product_id_by_name(ProductType.ROUTER)
mock_site = Site.from_subscription(router_creation_input_form_data["router_site"]).site
mock_v4 = faker.ipv4()
mock_v6 = faker.ipv6()
......@@ -156,7 +157,7 @@ def test_create_nokia_router_lso_failure(
)
# Run workflow
product_id = get_product_id_by_name(ProductType.NOKIA_ROUTER)
product_id = get_product_id_by_name(ProductType.ROUTER)
initial_router_data = [{"product": product_id}, router_creation_input_form_data]
result, process_stat, step_log = run_workflow("create_router", initial_router_data)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment