diff --git a/docs/source/conf.py b/docs/source/conf.py index 40f59bcb8bef6c57a7845a3340dbd07a7e44fa74..ec4ba29e5ae345953d6af2c5228c85f2a9763585 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ source_suffix = { } # -- Options for Markdown support -------------------------------------------- -myst_enable_extensions = ['attrs_block', 'deflist', 'replacements', 'smartquotes', 'strikethrough'] +myst_enable_extensions = ['attrs_block', 'deflist', 'replacements', 'smartquotes', 'strikethrough', 'fieldlist'] suppress_warnings = ['myst.strikethrough'] # -- Options for autodoc ----------------------------------------------------- @@ -33,7 +33,7 @@ html_logo = 'static/geant_logo_white.svg' # Both the class' and the __init__ method's docstring are concatenated and inserted. autoclass_content = 'both' -autodoc_typehints = 'none' +# autodoc_typehints = 'none' # Display todos by setting to True todo_include_todos = True diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py index 479e18097578ae6a9da77ec78f26d849c8c1cd69..13d90249b50917c899ef12c05f1d15758662b30c 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/provisioning_proxy.py @@ -22,45 +22,55 @@ from gso.products.product_types.device import DeviceProvisioning from gso.products.product_types.iptrunk import Iptrunk, IptrunkProvisioning logger = logging.getLogger(__name__) +"""{class}`logging.Logger` instance.""" DEFAULT_LABEL = "Provisioning proxy is running. Please come back later for the results." +"""The default label displayed when the provisioning proxy is running.""" class CUDOperation(strEnum): - """Enumerator for different C(R)UD operations that the provisioning proxy supports. + """Enumerator for different CRUD operations that the provisioning proxy supports. Read isn't applicable, hence these become CUD and not CRUD operations. """ - #: Creation is done with a POST request POST = "POST" - #: Updating is done with a PUT request + """Creation is done with a POST request.""" PUT = "PUT" - #: Removal is done with a DELETE request + """Updating is done with a PUT request.""" DELETE = "DELETE" + """Removal is done with a DELETE request.""" def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operation: CUDOperation) -> None: """Send a request to LSO. The callback address is derived using the process ID provided. - :param str endpoint: The LSO-specific endpoint to call, depending on the type of service object that's acted upon. - :param dict parameters: JSON body for the request, which will almost always at least consist of a subscription - object, and a boolean value to indicate a dry run. - :param UUIDstr process_id: The process ID that this request is a part of, used to call back to when the execution - of the playbook is completed. - :param :class:`CUDOperation` operation: The specific operation that's performed with the request. + :param endpoint: The LSO-specific endpoint to call, depending on the type of service object that's acted upon. + :type endpoint: str + :param parameters: JSON body for the request, which will almost always at least consist of a subscription object, + and a boolean value to indicate a dry run. + :type parameters: dict + :param process_id: The process ID that this request is a part of, used to call back to when the execution of the + playbook is completed. + :type process_id: UUIDstr + :param operation: The specific operation that's performed with the request. + :type operation: {class}`CUDOperation` + :rtype: None """ oss = settings.load_oss_params() pp_params = oss.PROVISIONING_PROXY assert pp_params + # Build up a callback URL for the Provisioning Proxy to return its results to. callback_url = f"{settings.load_oss_params().GENERAL.public_hostname}" f"/api/processes/{process_id}/resume" - logger.debug("[provisioning proxy] provisioning for process %s", process_id) + logger.debug(f"[provisioning proxy] provisioning for process {process_id}") + logger.debug(f"[provisioning proxy] Callback URL set to {callback_url}") parameters.update({"callback": callback_url}) url = f"{pp_params.scheme}://{pp_params.api_base}/api/{endpoint}" request = None + # Fire off the request, depending on the operation type. if operation == CUDOperation.POST: request = requests.post(url, json=parameters, timeout=10000) elif operation == CUDOperation.PUT: @@ -76,9 +86,13 @@ def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operatio def provision_device(subscription: DeviceProvisioning, process_id: UUIDstr, dry_run: bool = True) -> None: """Provision a new device using LSO. - :param :class:`DeviceProvisioning` subscription: The subscription object that's to be provisioned. - :param UUIDstr process_id: The related process ID, used for callback. - :param bool dry_run: A boolean indicating whether this should be a dry run or not, defaults to ``True``. + :param subscription: The subscription object that's to be provisioned. + :type subscription: {class}`DeviceProvisioning` + :param process_id: The related process ID, used for callback. + :type process_id: UUIDstr + :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`. + :type dry_run: bool + :rtype: None """ parameters = {"dry_run": dry_run, "subscription": json.loads(json_dumps(subscription))} @@ -90,10 +104,15 @@ def provision_ip_trunk( ) -> None: """Provision an IP trunk service using LSO. - :param :class:`IptrunkProvisioning` subscription: The subscription object that's to be provisioned. - :param UUIDstr process_id: The related process ID, used for callback. - :param str config_object: The type of object that's deployed - :param bool dry_run: A boolean indicating whether this should be a dry run or not, defaults to ``True``. + :param subscription: The subscription object that's to be provisioned. + :type subscription: {class}`IptrunkProvisioning` + :param process_id: The related process ID, used for callback. + :type process_id: UUIDstr + :param config_object: The type of object that's deployed. + :type config_object: str + :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`. + :type dry_run: bool + :rtype: None """ parameters = { "subscription": json.loads(json_dumps(subscription)), @@ -105,37 +124,16 @@ def provision_ip_trunk( _send_request("ip_trunk", parameters, process_id, CUDOperation.POST) -# def modify_ip_trunk(old_subscription: Iptrunk, -# new_subscription: Iptrunk, -# process_id: UUIDstr, -# dry_run: bool = True): -# """ -# Function that modifies an existing IP trunk subscription using LSO. -# -# :param :class:`Iptrunk` old_subscription: The subscription object, before -# its modification. -# :param :class:`Iptrunk` new_subscription: The subscription object, after -# modifications have been made to it. -# :param UUIDstr process_id: The related process ID, used for callback. -# :param bool dry_run: A boolean indicating whether this should be a dry ryn -# or not, defaults to ``True``. -# """ -# parameters = { -# 'dry_run': dry_run, -# 'old_subscription': old_subscription, -# 'subscription': new_subscription -# # ... missing parameters -# } -# -# _send_request('ip_trunk', parameters, process_id, CUDOperation.PUT) - - def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bool = True) -> None: """Deprovision an IP trunk service using LSO. - :param :class:`IptrunkProvisioning` subscription: The subscription object that's to be provisioned. - :param UUIDstr process_id: The related process ID, used for callback. - :param bool dry_run: A boolean indicating whether this should be a dry run or not, defaults to ``True``. + :param subscription: The subscription object that's to be provisioned. + :type subscription: {class}`IptrunkProvisioning` + :param process_id: The related process ID, used for callback. + :type process_id: UUIDstr + :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`. + :type dry_run: bool + :rtype: None """ parameters = {"subscription": json.loads(json_dumps(subscription)), "dry_run": dry_run, "verb": "terminate"} @@ -143,7 +141,24 @@ def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bo @inputstep("Await provisioning proxy results", assignee=Assignee("SYSTEM")) -def await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT_LABEL) -> FormGenerator: +def _await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT_LABEL) -> FormGenerator: + """Input step that forces the workflow to go into a `SUSPENDED` state. + + When the workflow is `SUSPENDED`, it will wait for user input to be presented before it continues running the next + steps of the workflow. User input is mimicked by the provisioning proxy, as it makes a `PUT` request to the callback + URL that it was given in `_send_request()`. This input is fabricated in such a way that it will advance the workflow + to the next step. This next step should always be `confirm_pp_results()`, where the operator is presented with the + output of the provisioning proxy. + + :param subscription: The current subscription that the provisioning proxy is acting on. + :type subscription: {class}`orchestrator.domain.SubscriptionModel` + :param label_text: A label that is displayed to the operator when the provisioning proxy has not returned its + results yet. Defaults to `DEFAULT_LABEL`. + :type label_text: str + :return: The input that is given by the provisioning proxy, that should contain run results, and a `confirm` + boolean set to `True`. + :rtype: {class}`orchestrator.types.FormGenerator` + """ class ProvisioningResultPage(FormPage): class Config: title = f"Deploying {subscription.product.name}..." @@ -165,11 +180,27 @@ def await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT_ @step("Reset Provisioning Proxy state") def reset_pp_success_state() -> State: + """Reset the boolean that indicates a successful provisioning proxy result in the state of a running workflow. + + :return: A new state of the workflow, where the key `pp_did_succeed` has been (re)set to false. + :rtype: {class}`orchestrator.types.State` + """ return {"pp_did_succeed": False} @inputstep("Confirm provisioning proxy results", assignee=Assignee("SYSTEM")) -def confirm_pp_results(state: State) -> FormGenerator: +def _confirm_pp_results(state: State) -> FormGenerator: + """Input step where a human has to confirm the result from calling provisioning proxy. + + The results of a call to the provisioning proxy are displayed, together with the fact whether this execution was + a success or not. If unsuccessful, an extra label is displayed that warns the user about the fact that this + execution will be retried. This will happen up to two times, after which the workflow will fail. + + :param state: The current state of the workflow. + :type state: {class}`orchestrator.types.State` + :return: Confirmation from the user, when presented with the run results. + :rtype: {class}`orchestrator.types.FormGenerator` + """ successful_run = state["pp_run_results"]["return_code"] == 0 class ConfirmRunPage(FormPage): @@ -194,10 +225,26 @@ def confirm_pp_results(state: State) -> FormGenerator: def pp_interaction(provisioning_step: Step) -> StepList: + """Wrapper function for an interaction with the provisioning proxy + + This method returns the three steps that make up an interaction with the provisioning proxy: + - The provisioning step itself, given by the user as input. + - The input step that suspends the workflow, and will wait for results from the provisioning proxy. + - An input step that presents the user with the results, where they must be confirmed. + + All these steps are wrapped in a {class}`orchestrator.workflow.conditional`. This ensures that when provisioning was + already successful, these steps are skipped. This mechanism is quite a dirty hack, and it is planned to be addressed + in a later release. + + :param provisioning_step: The step that executes an interaction with the provisioning proxy. + :type provisioning_step: {class}`orchestrator.workflow.Step` + :return: A list of three steps that form one interaction with the provisioning proxy. + :rtype: {class}`orchestrator.workflow.StepList` + """ should_retry_pp_steps = conditional(lambda state: not state.get("pp_did_succeed")) return ( should_retry_pp_steps(provisioning_step) - >> should_retry_pp_steps(await_pp_results) - >> should_retry_pp_steps(confirm_pp_results) + >> should_retry_pp_steps(_await_pp_results) + >> should_retry_pp_steps(_confirm_pp_results) )