Newer
Older
"""The Provisioning Proxy service, which interacts with :term:`LSO` running externally.
:term:`LSO` is responsible for executing Ansible playbooks, that deploy subscriptions.
from typing import Any
from orchestrator import step
from orchestrator.config.assignee import Assignee
from orchestrator.types import State
from orchestrator.utils.errors import ProcessFailureError
from orchestrator.workflow import Step, StepList, begin, callback_step, inputstep
from pydantic_forms.core import FormPage, ReadOnlyField
from pydantic_forms.types import FormGenerator
from pydantic_forms.validators import LongText
from gso import settings
def _send_request(parameters: dict, callback_route: str) -> None:
"""Send a request to :term:`LSO`. The callback address is derived using the process ID provided.
: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 callback_route: The callback route that should be used to resume the workflow.
:type callback_route: str
oss = settings.load_oss_params()
pp_params = oss.PROVISIONING_PROXY
# Build up a callback URL of the Provisioning Proxy to return its results to.
callback_url = f"{oss.GENERAL.public_hostname}{callback_route}"
debug_msg = f"[provisioning proxy] Callback URL set to {callback_url}"
logger.debug(debug_msg)
url = f"{pp_params.scheme}://{pp_params.api_base}/api/playbook"
request = requests.post(url, json=parameters, timeout=10)
request.raise_for_status()
def execute_playbook(
playbook_name: str,
callback_route: str,
inventory: dict[str, Any] | str,
extra_vars: dict[str, Any],
) -> None:
"""Execute a playbook remotely through the provisioning proxy.
When providing this method with an inventory, the format should be compatible with the Ansible YAML-based format.
For example, an inventory consisting of two hosts, which each a unique host variable assigned to them looks as
follows:
.. code-block:: json
"inventory": {
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"host1.local": {
"foo": "bar"
},
"host2.local": {
"key": "value"
}
}
}
}
.. warning::
Note the fact that the collection of all hosts is a dictionary, and not a list of strings. Ansible expects each
host to be a key-value pair. The key is the :term:`FQDN` of a host, and the value always ``null``.
The extra vars can be a simple dict consisting of key-value pairs, for example:
.. code-block:: json
"extra_vars": {
"dry_run": true,
"commit_comment": "I am a robot!",
"verb": "deploy"
}
:param str playbook_name: Filename of the playbook that is to be executed. It must be present on the remote system
running the provisioning proxy, otherwise it will return an error.
:param str callback_route: The endpoint at which :term:`GSO` expects a callback to continue the workflow executing
this step.
:param dict[str, Any] inventory: An inventory of machines at which the playbook is targeted. Must be in
YAML-compatible format.
:param dict[str, Any] extra_vars: Any extra variables that the playbook relies on. This can include a subscription
object, a boolean value indicating a dry run, a commit comment, etc.
"""
parameters = {
"playbook_name": playbook_name,
"inventory": inventory,
"extra_vars": extra_vars,
}
_send_request(parameters, callback_route)
Karel van Klink
committed
@step("Evaluate provisioning proxy result")
def _evaluate_pp_results(callback_result: dict) -> State:
if callback_result["return_code"] != 0:
raise ProcessFailureError(message="Provisioning proxy failure", details=callback_result)
return {"callback_result": callback_result}
Karel van Klink
committed
@step("Ignore provisioning proxy result")
def _ignore_pp_results(callback_result: dict) -> State:
return {"callback_result": callback_result}
@inputstep("Confirm provisioning proxy results", assignee=Assignee("SYSTEM"))
def _show_pp_results(state: State) -> FormGenerator:
if "callback_result" not in state:
return state
class ConfirmRunPage(FormPage):
class Config:
title: str = f"Execution for {state['subscription']['product']['name']} completed."
run_status: str = ReadOnlyField(state["callback_result"]["status"])
run_results: LongText = ReadOnlyField(json.dumps(state["callback_result"], indent=4))
yield ConfirmRunPage
state.pop("run_results")
def pp_interaction(provisioning_step: Step) -> StepList:
"""Interact with the provisioning proxy :term:`LSO` using a callback step.
An asynchronous interaction with the provisioning proxy. This is an external system that executes Ansible playbooks
to provision service subscriptions. If the playbook fails, this step will also fail, allowing for the user to retry
provisioning from the UI.
: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`
"""
Karel van Klink
committed
>> callback_step(
name=provisioning_step.name,
action_step=provisioning_step,
validate_step=_evaluate_pp_results,
)
>> _show_pp_results
)
Karel van Klink
committed
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def indifferent_pp_interaction(provisioning_step: Step) -> StepList:
"""Interact with the provisioning proxy :term:`LSO` using a callback step.
This interaction is identical from the one described in ``pp_interaction()``, with one functional difference.
Whereas the ``pp_interaction()`` will make the workflow step fail on unsuccessful interaction, this step will not.
It is therefore indifferent about the outcome of the Ansible playbook that is executed.
.. warning::
Using this interaction requires the operator to carefully evaluate the outcome of a playbook themselves. If a
playbook fails, this will not cause the workflow to fail.
: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=provisioning_step.name,
action_step=provisioning_step,
validate_step=_ignore_pp_results,
)
>> _show_pp_results
)