A creation workflow for adding a new router to the network.
To add a new router to the GÉANT network, the create_router
workflow must be executed first. The intake form for this
workflow requires the following fields to be filled in:
- Trouble ticket number
- Router vendor
- Router site
- Hostname
- Terminal server port
- Router role
The hostname is validated, by checking that the resulting FQDN is not already taken in IPAM.
Warning
The validation only checks whether the FQDN is already taken in IPAM, not whether it is registered somewhere on
the internet.
When the workflow is started, a subscription object is first instantiated in the service database, containing all the
information that was provided in the input form at the beginning. Then, the loopback addresses are allocated in IPAM,
which results in both the IPv4 and IPv6 addresses in the product model.
Once allocated, the first dry run of deploying router configuration takes place. An Ansible playbook is run, with all
the attributes of the new router. This is where GSO communicates with LSO, and the router configuration is checked, but
not committed to the machine.
After the dry run, the operator is presented with a view of the outcome of this playbook. This is their opportunity to
verify successful execution of the Ansible playbook, and whether the difference in configuration is as expected. If not,
this is their chance to abort the workflow, and no harm is done to the router.
When the operator confirms the outcome of this playbook execution, the playbook runs once again, but it will also commit
the configuration after checking. With the new router configured, the IPAM resources are verified to ensure this
external system is configured correctly.
If the new router is a Nokia, all its interfaces are added to Netbox. This is done to keep track of interface
reservations and bookkeeping. For Juniper routers, this does not need to take place. These existing devices are not
migrated into Netbox.
Finally, an Ansible playbook is run to verify that the connectivity and optical power levels of the router are in order.
Once this is completed, a checklist item is created in SharePoint and the router is taken into the PROVISIONING
state.
Gather information about the new router from the operator.
Source code in gso/workflows/router/create_router.py
| def initial_input_form_generator(product_name: str) -> FormGenerator:
"""Gather information about the new router from the operator."""
class CreateRouterForm(FormPage):
model_config = ConfigDict(title=product_name)
tt_number: TTNumber
partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type]
vendor: Vendor
router_site: active_site_selector() # type: ignore[valid-type]
hostname: str
ts_port: PortNumber
router_role: RouterRole
@model_validator(mode="after")
def hostname_must_be_available(self) -> Self:
router_site = self.router_site
if not router_site:
msg = "Please select a site before setting the hostname."
raise ValueError(msg)
selected_site = Site.from_subscription(router_site).site
input_fqdn = generate_fqdn(self.hostname, selected_site.site_name, selected_site.site_country_code)
if not infoblox.hostname_available(f"lo0.{input_fqdn}"):
msg = f'FQDN "{input_fqdn}" is not available.'
raise ValueError(msg)
return self
user_input = yield CreateRouterForm
user_input = user_input.model_dump()
summary_fields = ["hostname", "router_site", "vendor", "ts_port", "router_role"]
yield from create_summary_form(user_input, product_name, summary_fields)
return user_input
|
create_subscription(product, partner)
Create a new subscription object.
Source code in gso/workflows/router/create_router.py
| @step("Create subscription")
def create_subscription(product: UUIDstr, partner: str) -> State:
"""Create a new subscription object."""
subscription = RouterInactive.from_product_id(product, get_partner_by_name(partner).partner_id)
return {
"subscription": subscription,
"subscription_id": subscription.subscription_id,
}
|
initialize_subscription(subscription, hostname, ts_port, router_site, router_role, vendor)
Initialise the subscription object in the service database.
Source code in gso/workflows/router/create_router.py
| @step("Initialize subscription")
def initialize_subscription(
subscription: RouterInactive,
hostname: str,
ts_port: PortNumber,
router_site: str,
router_role: RouterRole,
vendor: Vendor,
) -> State:
"""Initialise the subscription object in the service database."""
subscription.router.router_ts_port = ts_port
subscription.router.router_site = Site.from_subscription(router_site).site
fqdn = generate_fqdn(
hostname,
subscription.router.router_site.site_name,
subscription.router.router_site.site_country_code,
)
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}"
return {"subscription": subscription}
|
ipam_allocate_loopback(subscription)
Allocate IPAM resources for the loopback interface.
Source code in gso/workflows/router/create_router.py
| @step("Allocate loopback interfaces in IPAM")
def ipam_allocate_loopback(subscription: RouterInactive) -> State:
"""Allocate IPAM resources for the loopback interface."""
fqdn = subscription.router.router_fqdn
if not fqdn:
msg = f"Router fqdn for subscription id {subscription.subscription_id} is missing!"
raise ValueError(msg)
loopback_v4, loopback_v6 = infoblox.allocate_host(f"lo0.{fqdn}", "LO", [fqdn], str(subscription.subscription_id))
subscription.router.router_lo_ipv4_address = loopback_v4
subscription.router.router_lo_ipv6_address = loopback_v6
subscription.router.router_lo_iso_address = iso_from_ipv4(subscription.router.router_lo_ipv4_address)
return {"subscription": subscription}
|
create_netbox_device(subscription)
Create a new Nokia device in Netbox.
Source code in gso/workflows/router/create_router.py
| @step("Create NetBox Device")
def create_netbox_device(subscription: RouterInactive) -> State:
"""Create a new Nokia device in Netbox."""
fqdn = subscription.router.router_fqdn
site_tier = subscription.router.router_site.site_tier if subscription.router.router_site else None
if not fqdn or not site_tier:
msg = f"FQDN and/or Site tier missing in router subscription {subscription.subscription_id}!"
raise ValueError(msg)
NetboxClient().create_device(fqdn, site_tier)
return {"subscription": subscription}
|
verify_ipam_loopback(subscription)
Validate the IPAM resources for the loopback interface.
Raises:
Type |
Description |
ProcessFailureError
|
If IPAM is configured incorrectly.
|
Source code in gso/workflows/router/create_router.py
| @step("Verify IPAM resources for loopback interface")
def verify_ipam_loopback(subscription: RouterInactive) -> None:
"""Validate the IPAM resources for the loopback interface.
Raises:
ProcessFailureError: If IPAM is configured incorrectly.
"""
host_record = infoblox.find_host_by_fqdn(f"lo0.{subscription.router.router_fqdn}")
if not host_record or str(subscription.subscription_id) not in host_record.comment:
msg = "Loopback record is incorrectly configured in IPAM, please investigate this manually!"
raise ProcessFailureError(msg)
|
prompt_reboot_router(subscription)
Wait for confirmation from an operator that the router has been rebooted.
Source code in gso/workflows/router/create_router.py
| @inputstep("Prompt to reboot", assignee=Assignee.SYSTEM)
def prompt_reboot_router(subscription: RouterInactive) -> FormGenerator:
"""Wait for confirmation from an operator that the router has been rebooted."""
class RebootPrompt(SubmitFormPage):
model_config = ConfigDict(title="Please reboot before continuing")
if subscription.router.router_site and subscription.router.router_site.site_ts_address:
info_label_1: Label = (
f"Base config has been deployed. Please log in via the console using https://"
f"{subscription.router.router_site.site_ts_address}."
)
else:
info_label_1 = "Base config has been deployed. Please log in via the console."
info_label_2: Label = "Reboot the router, and once it is up again, press submit to continue the workflow."
yield RebootPrompt
return {}
|
prompt_console_login()
Wait for confirmation from an operator that the router can be logged into.
Source code in gso/workflows/router/create_router.py
| @inputstep("Prompt to test the console", assignee=Assignee.SYSTEM)
def prompt_console_login() -> FormGenerator:
"""Wait for confirmation from an operator that the router can be logged into."""
class ConsolePrompt(SubmitFormPage):
model_config = ConfigDict(title="Verify local authentication")
info_label_1: Label = (
"Verify that you are able to log in to the router via the console using the admin account."
)
info_label_2: Label = "Once this is done, press submit to continue the workflow."
yield ConsolePrompt
return {}
|
prompt_insert_in_ims()
Wait for confirmation from an operator that the router has been inserted in IMS.
Source code in gso/workflows/router/create_router.py
| @inputstep("Prompt IMS insertion", assignee=Assignee.SYSTEM)
def prompt_insert_in_ims() -> FormGenerator:
"""Wait for confirmation from an operator that the router has been inserted in IMS."""
class IMSPrompt(SubmitFormPage):
model_config = ConfigDict(title="Update IMS mediation server")
info_label_1: Label = "Insert the router into IMS."
info_label_2: Label = "Once this is done, press submit to continue the workflow."
yield IMSPrompt
return {}
|
prompt_insert_in_radius(subscription)
Wait for confirmation from an operator that the router has been inserted in RADIUS.
Source code in gso/workflows/router/create_router.py
| @inputstep("Prompt RADIUS insertion", assignee=Assignee.SYSTEM)
def prompt_insert_in_radius(subscription: RouterInactive) -> FormGenerator:
"""Wait for confirmation from an operator that the router has been inserted in RADIUS."""
class RadiusPrompt(SubmitFormPage):
model_config = ConfigDict(title="Update RADIUS clients")
info_label_1: Label = (
f"Please go to https://kratos.geant.org/add_radius_client and add the {subscription.router.router_fqdn}"
f" - {subscription.router.router_lo_ipv4_address} to radius authentication"
)
info_label_2: Label = "This will be functionally checked later during verification work."
yield RadiusPrompt
return {}
|
create_new_sharepoint_checklist(subscription, tt_number, process_id)
Create a new checklist in SharePoint for approving this router.
Source code in gso/workflows/router/create_router.py
| @step("Create a new SharePoint checklist")
def create_new_sharepoint_checklist(subscription: RouterProvisioning, tt_number: str, process_id: UUIDstr) -> State:
"""Create a new checklist in SharePoint for approving this router."""
new_list_item_url = SharePointClient().add_list_item(
list_name="p_router",
fields={
"Title": subscription.router.router_fqdn,
"TT_NUMBER": tt_number,
"GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
},
)
return {"checklist_url": new_list_item_url}
|
create_router()
Create a new router in the service database.
- Create and initialise the subscription object in the service database
- Allocate IPAM resources for the loopback interface
- Deploy configuration on the new router, first as a dry run
- Validate IPAM resources
- Create a new device in Netbox
Source code in gso/workflows/router/create_router.py
| @workflow(
"Create router",
initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
target=Target.CREATE,
)
def create_router() -> StepList:
"""Create a new router in the service database.
* Create and initialise the subscription object in the service database
* Allocate IPAM resources for the loopback interface
* Deploy configuration on the new router, first as a dry run
* Validate IPAM resources
* Create a new device in Netbox
"""
router_is_nokia = conditional(lambda state: state["vendor"] == Vendor.NOKIA)
router_is_pe = conditional(lambda state: state["router_role"] == RouterRole.PE)
return (
begin
>> create_subscription
>> store_process_subscription(Target.CREATE)
>> initialize_subscription
>> ipam_allocate_loopback
>> router_is_pe(create_kentik_device)
>> lso_interaction(deploy_base_config_dry)
>> lso_interaction(deploy_base_config_real)
>> verify_ipam_loopback
>> prompt_reboot_router
>> prompt_console_login
>> prompt_insert_in_ims
>> prompt_insert_in_radius
>> router_is_nokia(create_netbox_device)
>> lso_interaction(run_checks_after_base_config)
>> set_status(SubscriptionLifecycle.PROVISIONING)
>> create_new_sharepoint_checklist
>> prompt_sharepoint_checklist_url
>> resync
>> done
)
|