diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py index fd0f8685bd0d08c6338d1d504362481152394bb4..a3e9f2f68ceadf750ae8c2f9b7972b2da328048e 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/provisioning_proxy.py @@ -284,9 +284,17 @@ def _show_pp_results(state: State) -> FormGenerator: return state -def pp_interaction(provisioning_step: Step, interaction_name: str) -> StepList: +def pp_interaction(provisioning_step: Step) -> StepList: + """ + An interaction with the provisioning proxy :term:`LSO` as a callback step. + + :param provisioning_step: A workflow step that performs an operation remotely using the provisioning proxy. + :type provisioning_step: :class:`Step` + :return: A list of steps that is executed as part of the workflow. + :rtype: :class:`StepList` + """ return ( begin - >> callback_step(name=interaction_name, action_step=provisioning_step, validate_step=_evaluate_pp_results) + >> callback_step(name=provisioning_step.name, action_step=provisioning_step, validate_step=_evaluate_pp_results) >> _show_pp_results ) diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index fabfddecfd69aa90151d331bf0c0a205eedda26b..aa09bc82b46de0bba0ca2352d4c1992cdeb140ed 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -21,10 +21,11 @@ class LAGMember(BaseModel): interface_description: str def __hash__(self) -> int: + # TODO: check if this is still needed return hash((self.interface_name, self.interface_description)) -@step("[COMMIT] Set ISIS metric to 90000") +@step("[COMMIT] Set ISIS metric to 90.000") def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: old_isis_metric = subscription.iptrunk.iptrunk_isis_metric subscription.iptrunk.iptrunk_isis_metric = 90000 @@ -33,7 +34,6 @@ def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str return { "subscription": subscription, "old_isis_metric": old_isis_metric, - "label_text": "ISIS is being set to 90K by the provisioning proxy, please wait for the results", } diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index e87e4a9fc17ccd1a570dd6ea097cdd4e19ed2bad..ff83e7c3a464b662ed36bebad5e2546fa48376b0 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -221,63 +221,57 @@ def initialize_subscription( @step("Provision IP trunk interface [DRY RUN]") -def provision_ip_trunk_iface_dry(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface") +def provision_ip_trunk_iface_dry( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "trunk_interface", True) - return { - "subscription": subscription, - "label_text": "[DRY RUN] Provisioning a trunk interface, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Provision IP trunk interface [FOR REAL]") -def provision_ip_trunk_iface_real(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface", False) +def provision_ip_trunk_iface_real( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "trunk_interface", False) - return { - "subscription": subscription, - "label_text": "Provisioning a trunk interface, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Check IP connectivity of the trunk") -def check_ip_trunk_connectivity(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.check_ip_trunk(subscription, process_id, tt_number, "ping") +def check_ip_trunk_connectivity( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.check_ip_trunk(subscription, process_id, callback_route, tt_number, "ping") - return { - "subscription": subscription, - "label_text": "[CHECK] Checking IP traffic flow on the trunk, to get the results of the playbook.", - } + return {"subscription": subscription} @step("Provision IP trunk ISIS interface [DRY RUN]") -def provision_ip_trunk_isis_iface_dry(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface") +def provision_ip_trunk_isis_iface_dry( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface") - return { - "subscription": subscription, - "label_text": "[DRY RUN] Provisioning ISIS interfaces, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Provision IP trunk ISIS interface [FOR REAL]") -def provision_ip_trunk_isis_iface_real(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False) +def provision_ip_trunk_isis_iface_real( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface", False) - return { - "subscription": subscription, - "label_text": "[COMMIT] Provisioning ISIS interfaces, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Check ISIS adjacency") -def check_ip_trunk_isis(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.check_ip_trunk(subscription, process_id, tt_number, "isis") +def check_ip_trunk_isis( + subscription: IptrunkProvisioning, callback_route: str, process_id: UUIDstr, tt_number: str +) -> State: + provisioning_proxy.check_ip_trunk(subscription, process_id, callback_route, tt_number, "isis") - return { - "subscription": subscription, - "label_text": "[CHECK] Checking ISIS adjacency, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("NextBox integration") @@ -342,12 +336,12 @@ def create_iptrunk() -> StepList: >> initialize_subscription >> get_info_from_ipam >> reserve_interfaces_in_netbox - >> pp_interaction(provision_ip_trunk_iface_dry, "Provision IPtrunk interface [DRY RUN]") - >> pp_interaction(provision_ip_trunk_iface_real, "Provision IPtrunk interface [FOR REAL]") - >> pp_interaction(check_ip_trunk_connectivity, "Check IPtrunk connectivity") - >> pp_interaction(provision_ip_trunk_isis_iface_dry, "Provision ISIS interface [DRY RUN]") - >> pp_interaction(provision_ip_trunk_isis_iface_real, "Provision ISIS interface [FOR REAL]") - >> pp_interaction(check_ip_trunk_isis, "Validate IPtrunk") + >> pp_interaction(provision_ip_trunk_iface_dry) + >> pp_interaction(provision_ip_trunk_iface_real) + >> pp_interaction(check_ip_trunk_connectivity) + >> pp_interaction(provision_ip_trunk_isis_iface_dry) + >> pp_interaction(provision_ip_trunk_isis_iface_real) + >> pp_interaction(check_ip_trunk_isis) >> allocate_interfaces_in_netbox >> set_status(SubscriptionLifecycle.ACTIVE) >> resync diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 18a57cf161846b03aef12f2e704e9f65ffe0ff92..0d238437f4ec9f8016c0e693be1ea8fd201c13ff 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -152,6 +152,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @step("[DRY RUN] Disable configuration on old router") def disable_old_config_dry( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -166,6 +167,7 @@ def disable_old_config_dry( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "deactivate", "deactivate", @@ -173,13 +175,13 @@ def disable_old_config_dry( return { "subscription": subscription, - "label_text": "[DRY RUN] Disable config on old node, please refresh to get the results of the playbook.", } @step("[REAL] Disable configuration on old router") def disable_old_config_real( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -194,6 +196,7 @@ def disable_old_config_real( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "deactivate", "deactivate", @@ -202,13 +205,13 @@ def disable_old_config_real( return { "subscription": subscription, - "label_text": "Disable config on the old node, please refresh to get the results of the playbook.", } @step("[DRY RUN] Deploy configuration on new router") def deploy_new_config_dry( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -223,6 +226,7 @@ def deploy_new_config_dry( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "deploy", "trunk_interface", @@ -232,13 +236,13 @@ def deploy_new_config_dry( return { "subscription": subscription, - "label_text": "[DRY RUN] Deploying new trunk interface, please refresh to get the results of the playbook.", } @step("Deploy configuration on new router") def deploy_new_config_real( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -253,6 +257,7 @@ def deploy_new_config_real( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "deploy", "trunk_interface", @@ -263,7 +268,6 @@ def deploy_new_config_real( return { "subscription": subscription, - "label_text": "[COMMIT] Deploying new trunk interface, please refresh to get the results of the playbook.", } @@ -289,6 +293,7 @@ def confirm_continue_move_fiber() -> FormGenerator: @step("Deploy ISIS configuration on new router") def deploy_new_isis( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -303,6 +308,7 @@ def deploy_new_isis( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "deploy", "isis_interface", @@ -313,7 +319,6 @@ def deploy_new_isis( return { "subscription": subscription, - "label_text": "Deploy ISIS config on the new router, please refresh to get the results of the playbook.", } @@ -333,9 +338,11 @@ def confirm_continue_restore_isis() -> FormGenerator: @step("Restore ISIS metric to original value") -def restore_isis_metric(subscription: Iptrunk, process_id: UUIDstr, tt_number: str, old_isis_metric: int) -> State: +def restore_isis_metric( + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str, old_isis_metric: int +) -> State: subscription.iptrunk.iptrunk_isis_metric = old_isis_metric - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False) + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface", False) return {"subscription": subscription} @@ -343,6 +350,7 @@ def restore_isis_metric(subscription: Iptrunk, process_id: UUIDstr, tt_number: s @step("[DRY RUN] Delete configuration on old router") def delete_old_config_dry( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -357,6 +365,7 @@ def delete_old_config_dry( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "delete", "delete", @@ -364,16 +373,13 @@ def delete_old_config_dry( logger.warning("Playbook verb is not yet properly set.") - return { - "subscription": subscription, - "label_text": "[DRY RUN] Removing configuration from old router," - "please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Delete configuration on old router") def delete_old_config_real( subscription: Iptrunk, + callback_route: str, new_node: Router, new_lag_interface: str, new_lag_member_interfaces: list[dict], @@ -388,6 +394,7 @@ def delete_old_config_real( new_lag_member_interfaces, replace_index, process_id, + callback_route, tt_number, "delete", "delete", @@ -396,10 +403,7 @@ def delete_old_config_real( logger.warning("Playbook verb is not yet properly set.") - return { - "subscription": subscription, - "label_text": "Removing configuration from old router, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Update IPAM") @@ -506,17 +510,17 @@ def migrate_iptrunk() -> StepList: >> store_process_subscription(Target.MODIFY) >> unsync >> reserve_interfaces_in_netbox - >> pp_interaction(set_isis_to_90000, "Set ISIS metric to 90.000") - >> pp_interaction(disable_old_config_dry, "Disable old configuration [DRY RUN]") - >> pp_interaction(disable_old_config_real, "Disable old configuration [FOR REAL]") - >> pp_interaction(deploy_new_config_dry, "Deploy new configuration [DRY RUN]") - >> pp_interaction(deploy_new_config_real, "Deploy new configuration [FOR REAL]") + >> pp_interaction(set_isis_to_90000) + >> pp_interaction(disable_old_config_dry) + >> pp_interaction(disable_old_config_real) + >> pp_interaction(deploy_new_config_dry) + >> pp_interaction(deploy_new_config_real) >> confirm_continue_move_fiber - >> pp_interaction(deploy_new_isis, "Deploy new ISIS interface") + >> pp_interaction(deploy_new_isis) >> confirm_continue_restore_isis - >> pp_interaction(restore_isis_metric, "Restore original ISIS metric") - >> pp_interaction(delete_old_config_dry, "Delete old configuration [DRY RUN]") - >> pp_interaction(delete_old_config_real, "Delete old configuration [FOR REAL]") + >> pp_interaction(restore_isis_metric) + >> pp_interaction(delete_old_config_dry) + >> pp_interaction(delete_old_config_real) >> update_ipam >> update_subscription_model >> update_netbox diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py index dedbb36596d53a2a2d892d80097bce0fb11664bc..910bec4fcfb65fc463bdbd3042f003cda329460c 100644 --- a/gso/workflows/iptrunk/modify_isis_metric.py +++ b/gso/workflows/iptrunk/modify_isis_metric.py @@ -30,25 +30,21 @@ def modify_iptrunk_subscription(subscription: Iptrunk, isis_metric: int) -> Stat @step("Provision IP trunk ISIS interface [DRY RUN]") -def provision_ip_trunk_isis_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface") +def provision_ip_trunk_isis_iface_dry( + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface") - return { - "subscription": subscription, - "label_text": "This is a dry run for the deployment of a new IP trunk ISIS interface. Deployment is being taken" - " care of by the provisioning proxy, please wait for the results to come back before continuing.", - } + return {"subscription": subscription} @step("Provision IP trunk ISIS interface [FOR REAL]") -def provision_ip_trunk_isis_iface_real(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False) +def provision_ip_trunk_isis_iface_real( + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface", False) - return { - "subscription": subscription, - "label_text": "This is a live deployment of a new IP trunk ISIS interface. Deployment is being taken care of by" - " the provisioning proxy, please wait for the results to come back before continuing.", - } + return {"subscription": subscription} @workflow( @@ -62,8 +58,8 @@ def modify_isis_metric() -> StepList: >> store_process_subscription(Target.MODIFY) >> unsync >> modify_iptrunk_subscription - >> pp_interaction(provision_ip_trunk_isis_iface_dry, "Provision ISIS interface [DRY RUN]") - >> pp_interaction(provision_ip_trunk_isis_iface_real, "Provision ISIS interface [FOR REAL]") + >> pp_interaction(provision_ip_trunk_isis_iface_dry) + >> pp_interaction(provision_ip_trunk_isis_iface_real) >> resync >> done ) diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 6a94ac674e37d03c9c26f809617400f4be647a54..908b20e295db94d50b4a52627a924890a98e0c0f 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -178,30 +178,24 @@ def modify_iptrunk_subscription( @step("Provision IP trunk interface [DRY RUN]") def provision_ip_trunk_iface_dry( - subscription: Iptrunk, process_id: UUIDstr, tt_number: str, removed_ae_members: List[str] + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str, removed_ae_members: List[str] ) -> State: provisioning_proxy.provision_ip_trunk( - subscription, process_id, tt_number, "trunk_interface", True, removed_ae_members + subscription, process_id, callback_route, tt_number, "trunk_interface", True, removed_ae_members ) - return { - "subscription": subscription, - "label_text": "[DRY RUN] Provisioning trunk interface, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Provision IP trunk interface [FOR REAL]") def provision_ip_trunk_iface_real( - subscription: Iptrunk, process_id: UUIDstr, tt_number: str, removed_ae_members: List[str] + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str, removed_ae_members: List[str] ) -> State: provisioning_proxy.provision_ip_trunk( - subscription, process_id, tt_number, "trunk_interface", False, removed_ae_members + subscription, process_id, callback_route, tt_number, "trunk_interface", False, removed_ae_members ) - return { - "subscription": subscription, - "label_text": "Provisioning trunk interface, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Update interfaces in Netbox. Reserving interfaces.") @@ -279,8 +273,8 @@ def modify_trunk_interface() -> StepList: >> unsync >> modify_iptrunk_subscription >> update_interfaces_in_netbox - >> pp_interaction(provision_ip_trunk_iface_dry, "Provision IPtrunk interface [DRY RUN]") - >> pp_interaction(provision_ip_trunk_iface_real, "Provision IPtrunk interface [FOR REAL") + >> pp_interaction(provision_ip_trunk_iface_dry) + >> pp_interaction(provision_ip_trunk_iface_real) >> allocate_interfaces_in_netbox >> resync >> done diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index 3cd0f74424d46704e91c3be9ac0057b748dffc55..0c8014e4c0e45fc8624467579f93e3f3c913a92d 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -29,33 +29,26 @@ def initial_input_form_generator() -> FormGenerator: @step("Drain traffic from trunk") -def drain_traffic_from_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False) - return { - "subscription": subscription, - "label_text": "This is setting the ISIS metric of the trunk to 9000. Press refresh to get the results." - "When traffic is drained, confirm to continue.", - } +def drain_traffic_from_ip_trunk( + subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str +) -> State: + provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface", False) + + return {"subscription": subscription} @step("Deprovision IP trunk [DRY RUN]") -def deprovision_ip_trunk_dry(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.deprovision_ip_trunk(subscription, process_id, tt_number, True) +def deprovision_ip_trunk_dry(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State: + provisioning_proxy.deprovision_ip_trunk(subscription, process_id, callback_route, tt_number, True) - return { - "subscription": subscription, - "label_text": "[DRY RUN] Terminating IP trunk, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Deprovision IP trunk [FOR REAL]") -def deprovision_ip_trunk_real(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.deprovision_ip_trunk(subscription, process_id, tt_number, False) +def deprovision_ip_trunk_real(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State: + provisioning_proxy.deprovision_ip_trunk(subscription, process_id, callback_route, tt_number, False) - return { - "subscription": subscription, - "label_text": "[COMMIT] Terminating IP trunk, please refresh to get the results of the playbook.", - } + return {"subscription": subscription} @step("Deprovision IPv4 networks") @@ -83,9 +76,9 @@ def terminate_iptrunk() -> StepList: config_steps = ( init - >> pp_interaction(set_isis_to_90000, "Set ISIS metric to 90.000") - >> pp_interaction(deprovision_ip_trunk_dry, "Deprovision IPtrunk [DRY RUN]") - >> pp_interaction(deprovision_ip_trunk_real, "Deprovision IPtrunk [FOR REAL]") + >> pp_interaction(set_isis_to_90000) + >> pp_interaction(deprovision_ip_trunk_dry) + >> pp_interaction(deprovision_ip_trunk_real) ) ipam_steps = init >> deprovision_ip_trunk_ipv4 >> deprovision_ip_trunk_ipv6 diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 7e18161d7a95a0c04df65b0faaa94b8cba9fe8c2..1f452e053cbf4ac225a2fbeddc8f12f2eec88cbb 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -153,8 +153,8 @@ def create_netbox_device(subscription: RouterProvisioning) -> State: subscription.router.router_fqdn, str(subscription.router.router_site.site_tier), # type: ignore[union-attr] ) - return {"subscription": subscription, "label_text": "Creating NetBox device"} - return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."} + return {"subscription": subscription} + return {"subscription": subscription} @step("Verify IPAM resources for loopback interface") @@ -210,8 +210,8 @@ def create_router() -> StepList: >> initialize_subscription >> ipam_allocate_loopback >> should_allocate_ias(ipam_allocate_ias_networks) - >> pp_interaction(provision_router_dry, "Provision new router [DRY RUN]") - >> pp_interaction(provision_router_real, "Provision new router [FOR REAL]") + >> pp_interaction(provision_router_dry) + >> pp_interaction(provision_router_real) >> verify_ipam_loopback >> should_allocate_ias(verify_ipam_ias) >> create_netbox_device