Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • goat/gap/geant-service-orchestrator
1 result
Show changes
Commits on Source (28)
Showing
with 304 additions and 124 deletions
......@@ -34,4 +34,4 @@ lint-documentation:
- vale sync
script:
- vale --glob='!*/_?ipam.py|!*/services/README\.md|!*/apidocs/*|!*/migrations/*' $CI_PROJECT_DIR/docs/source $CI_PROJECT_DIR/gso
- vale --glob='!*/migrations/*' $CI_PROJECT_DIR/docs/source $CI_PROJECT_DIR/gso
......@@ -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 -----------------------------------------------------
......@@ -21,6 +21,8 @@ autodoc2_packages = [
"../../gso"
]
autodoc2_render_plugin = "myst"
autodoc2_hidden_objects = ["undoc", "inherited"]
autodoc2_index_template = None
# -- Options for HTML output -------------------------------------------------
html_theme = 'sphinx_rtd_theme'
......@@ -33,7 +35,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
......
# Glossary of terms
{.glossary}
CRUD
: Create, Read, Update, Delete
FQDN
: Fully Quantified Domain Name
GSO
: GÉANT Service Orchestrator
IPAM
: IP Address Management
IS-IS
: Intermediate System to Intermediate System: a routing protocol described in
<a href="https://datatracker.ietf.org/doc/html/rfc7142" target="_blank">RFC 7142</a>.
ISO
: International Organisation for Standardisation
LSO
: Lightweight Service Orchestrator
NET
: Network Entity Title: used for {term}`IS-IS` routing.
WFO
: <a href="https://workfloworchestrator.org/" target="_blank">Workflow Orchestrator</a>
......@@ -9,14 +9,24 @@ Packages = proselint, Microsoft
[*.{md,py}]
; We only lint .md and .py files
BasedOnStyles = Vale, proselint, Microsoft
; Some headers are generated and we have no real influence over them
Microsoft.Headings = NO
; Found to be too intrusive
Microsoft.Passive = NO
; We are not a general audience
Microsoft.GeneralURL = NO
; It's okay to leave TODOs in the code, that's what they're for
proselint.Annotations = NO
; Replacing a ... with … shouldn't be holding back the entire CI pipeline
proselint.Typography = warning
; Same applies for not using contractions
Microsoft.Contractions = warning
TokenIgnores = (?:{term}`\S+`)
TokenIgnores = ({term}), (:param \S+:), (:type \S+:)
[*/glossary.md]
; Ignore acronyms being undefined in the file that defines all acronyms by definition.
Microsoft.Acronyms = NO
[formats]
; Ignore inline comments in source code, as these do not show up in generated documentation.
py = rst
py = md
......@@ -2,3 +2,4 @@ toctree
[Ss]ubpackages
virtualenv
[Pp]revious
mypy
......@@ -8,3 +8,4 @@ Ansible
API
dry_run
Dark_fiber
[A|a]ddress
"""Product blocks that store information about subscriptions.
In this file, some enumerators may be declared that are available for use across all subscriptions.
"""
from enum import Enum
class PhyPortCapacity(Enum):
ONE = "1g"
TEN = "10g"
HUNDRED = "100g"
FOUR_HUNDRED = "400g"
"""Physical port capacity enumerator.
An enumerator that has the different possible capacities of ports that are available to use in subscriptions.
"""
ONE = "1G"
"""1Gbps"""
TEN = "10G"
"""10Gbps"""
HUNDRED = "100G"
"""100Gbps"""
FOUR_HUNDRED = "400G"
"""400Gbps"""
"""Product block for {class}`Device` products."""
import ipaddress
from typing import Optional
......@@ -8,19 +9,30 @@ from gso.products.product_blocks.site import SiteBlock, SiteBlockInactive, SiteB
class DeviceVendor(strEnum):
juniper = "juniper"
nokia = "nokia"
"""Enumerator for the different product vendors that are supported."""
JUNIPER = "juniper"
"""Juniper devices."""
NOKIA = "nokia"
"""Nokia devices."""
class DeviceRole(strEnum):
p = "p"
pe = "pe"
amt = "amt"
"""Enumerator for the different types of routers."""
P = "p"
"""P router."""
PE = "pe"
"""PE router."""
AMT = "amt"
"""AMT router."""
class DeviceBlockInactive(
ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="DeviceBlock"
):
"""A device that's being currently inactive. See {class}`DeviceBlock`."""
device_fqdn: Optional[str] = None
device_ts_address: Optional[str] = None
device_ts_port: Optional[int] = None
......@@ -37,6 +49,8 @@ class DeviceBlockInactive(
class DeviceBlockProvisioning(DeviceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
"""A device that's being provisioned. See {class}`DeviceBlock`."""
device_fqdn: str
device_ts_address: str
device_ts_port: int
......@@ -53,16 +67,34 @@ class DeviceBlockProvisioning(DeviceBlockInactive, lifecycle=[SubscriptionLifecy
class DeviceBlock(DeviceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
"""A device that's currently deployed in the network."""
device_fqdn: str
"""{term}`FQDN` of a device."""
device_ts_address: str
"""The address of the terminal server that this device is connected to. The terminal server provides out of band
access. This is required in case a link goes down, or when a device is initially added to the network and it does
not have any IP trunks connected to it yet."""
device_ts_port: int
"""The port of the terminal server that this device is connected to. Used for the same reason as mentioned
previously."""
device_access_via_ts: bool
"""Whether this device should be accessed through the terminal server, or through its loopback address."""
device_lo_ipv4_address: ipaddress.IPv4Address
"""The IPv4 loopback address of the device."""
device_lo_ipv6_address: ipaddress.IPv6Address
"""The IPv6 loopback address of the device."""
device_lo_iso_address: str
"""The {term}`ISO` {term}`NET` of the device, used for {term}`IS-IS` support."""
device_si_ipv4_network: ipaddress.IPv4Network
"""The SI IPv4 network of the device."""
device_ias_lt_ipv4_network: ipaddress.IPv4Network
"""The IAS LT IPv4 network of the device."""
device_ias_lt_ipv6_network: ipaddress.IPv6Network
"""The IAS LT IPv6 network of the device."""
device_vendor: DeviceVendor
"""The vendor of the device, can be any of the values defined in {class}`DeviceVendor`."""
device_role: DeviceRole
"""The role of the device, which can be any of the values defined in {class}`DeviceRole`."""
device_site: SiteBlock
"""The {class}`Site` that this device resides in. Both physically and computationally."""
"""IP trunk product block that has all parameters of a subscription throughout its lifecycle."""
import ipaddress
from typing import Optional
......@@ -9,13 +11,15 @@ from gso.products.product_blocks.device import DeviceBlock, DeviceBlockInactive,
class IptrunkType(strEnum):
Dark_fiber = "Dark_fiber"
Leased = "Leased"
DARK_FIBER = "Dark_fiber"
LEASED = "Leased"
class IptrunkBlockInactive(
ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkBlock"
):
"""A trunk that's currently inactive, see {class}`IptrunkBlock`."""
geant_s_sid: Optional[str] = None
iptrunk_description: Optional[str] = None
iptrunk_type: Optional[IptrunkType] = None
......@@ -39,6 +43,8 @@ class IptrunkBlockInactive(
class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
"""A trunk that's currently being provisioned, see {class}`IptrunkBlock`."""
geant_s_sid: Optional[str] = None
iptrunk_description: Optional[str] = None
iptrunk_type: Optional[IptrunkType] = None
......@@ -62,22 +68,39 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife
class IptrunkBlock(IptrunkBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
"""A trunk that's currently deployed in the network."""
geant_s_sid: str
"""GÉANT service ID associated with this trunk. """
iptrunk_description: str
"""A human-readable description of this trunk."""
iptrunk_type: IptrunkType
iptrunk_speed: str
"""The type of trunk, can be either dark fibre or leased capacity."""
iptrunk_speed: str # FIXME: should be of PhyPortCapacity type
"""The speed of the trunk, measured per interface associated with it."""
iptrunk_minimum_links: int
"""The minimum amount of links the trunk should consist of."""
iptrunk_isis_metric: int
"""The {term}`IS-IS` metric of this link"""
iptrunk_ipv4_network: ipaddress.IPv4Network
"""The IPv4 network used for this trunk."""
iptrunk_ipv6_network: ipaddress.IPv6Network
"""The IPv6 network used for this trunk."""
#
iptrunk_sideA_node: DeviceBlock
"""The router that hosts the A side of the trunk."""
iptrunk_sideA_ae_iface: str
"""The name of the interface on which the trunk connects."""
iptrunk_sideA_ae_geant_a_sid: str
"""The service ID of the interface."""
iptrunk_sideA_ae_members: list[str] = Field(default_factory=list)
"""A list of interface members that make up the aggregated Ethernet interface."""
iptrunk_sideA_ae_members_description: list[str] = Field(default_factory=list)
"""The list of descriptions that describe the list of interface members."""
#
iptrunk_sideB_node: DeviceBlock
"""The router that hosts the B side of the trunk. It possesses the same attributes as the A-side, including the
interfaces and its descriptions."""
iptrunk_sideB_ae_iface: str
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: list[str] = Field(default_factory=list)
......
......@@ -5,10 +5,10 @@ from orchestrator.types import SubscriptionLifecycle, strEnum
class SiteTier(strEnum):
tier1 = 1
tier2 = 2
tier3 = 3
tier4 = 4
TIER1 = 1
TIER2 = 2
TIER3 = 3
TIER4 = 4
class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"):
......
<!-- vale off -->
## IPAM
### Example configuration
......
"""External services that the service orchestrator can interact with."""
......@@ -246,7 +246,7 @@ def _allocate_host(
if couldn't allocate host due to requested network not existing.
"""
# TODO: should hostnames be unique
# (i.e. fail if hostname already exists in this domain/service)?
# (that is, fail if hostname already exists in this domain/service)?
assert addrs or networks, "You must specify either the host addresses or the networks CIDR."
oss = settings.load_oss_params()
assert oss.IPAM.INFOBLOX
......@@ -563,7 +563,7 @@ def delete_service_host(
assert "_ref" in host_data[0]
host_ref = host_data[0]["_ref"]
# Find cname records reference
# Find CNAME records reference
r = requests.get(
f"{_wapi(infoblox_params)}/record:cname",
params={
......
"""The Provisioning Proxy service, which interacts with LSO running externally.
"""The Provisioning Proxy service, which interacts with {term}`LSO` running externally.
LSO is responsible for executing Ansible playbooks, that deploy subscriptions.
{term}`LSO` is responsible for executing Ansible playbooks, that deploy subscriptions.
"""
import json
import logging
from typing import NoReturn
import requests
from orchestrator import inputstep
from orchestrator import conditional, inputstep, step
from orchestrator.config.assignee import Assignee
from orchestrator.domain import SubscriptionModel
from orchestrator.forms import FormPage, ReadOnlyField
from orchestrator.forms.validators import Accept, Label, LongText
from orchestrator.types import FormGenerator, State, UUIDstr, strEnum
from orchestrator.utils.json import json_dumps
from orchestrator.workflow import Step, StepList, abort
from pydantic import validator
from gso import settings
......@@ -20,44 +22,56 @@ 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 {term}`CRUD` operations that the provisioning proxy supports.
Read isn't applicable, hence these become CUD and not CRUD operations.
Read isn't applicable, hence the missing R.
"""
#: 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.
"""Send a request to {term}`LSO`. The callback address is derived using the process ID provided.
:param endpoint: The {term}`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 of 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:
......@@ -71,11 +85,15 @@ 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``.
"""Provision a new device using {term}`LSO`.
: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))}
......@@ -85,12 +103,17 @@ def provision_device(subscription: DeviceProvisioning, process_id: UUIDstr, dry_
def provision_ip_trunk(
subscription: IptrunkProvisioning, process_id: UUIDstr, config_object: str, dry_run: bool = True
) -> 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``.
"""Provision an IP trunk service using {term}`LSO`.
: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)),
......@@ -102,37 +125,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``.
"""Deprovision an IP trunk service using {term}`LSO`.
: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"}
......@@ -140,7 +142,25 @@ 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) -> 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's displayed to the operator when the provisioning proxy hasn't returned its
results yet. Defaults to `DEFAULT_LABEL`.
:type label_text: str
:return: The input that's 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}..."
......@@ -150,7 +170,7 @@ def await_pp_results(subscription: SubscriptionModel, label_text: str) -> FormGe
confirm: Accept = Accept("INCOMPLETE")
@validator("pp_run_results", allow_reuse=True, pre=True, always=True)
def run_results_must_be_given(cls, run_results: dict) -> dict | None:
def run_results_must_be_given(cls, run_results: dict) -> dict | NoReturn:
if run_results is None:
raise ValueError("Run results may not be empty. Wait for the provisioning proxy to finish.")
return run_results
......@@ -160,8 +180,31 @@ def await_pp_results(subscription: SubscriptionModel, label_text: str) -> FormGe
return result_page.dict()
@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):
class Config:
title = (
......@@ -170,10 +213,56 @@ def confirm_pp_results(state: State) -> FormGenerator:
f"completed, please confirm the results below."
)
if not successful_run:
pp_retry_label1: Label = (
"Provisioning Proxy - playbook execution failed: inspect the output before proceeding" # type: ignore
)
run_status: str = ReadOnlyField(state["pp_run_results"]["status"])
run_results: LongText = ReadOnlyField(f"{state['pp_run_results']['output']}")
confirm: Accept = Accept("INCOMPLETE")
if not successful_run:
pp_retry_label: Label = (
"Click submit to retry. Otherwise, abort the workflow from the process tab." # type: ignore
)
yield ConfirmRunPage
return state
return {"pp_did_succeed": successful_run}
def pp_interaction(provisioning_step: Step, attempts: int) -> StepList:
"""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's planned to be addressed
in a later release.
The parameter `attempts` indicates how many times a provisioning may be attempted. When this amount is exceeded, and
it's still not successful, the workflow will be aborted.
:param provisioning_step: The step that executes an interaction with the provisioning proxy.
:type provisioning_step: {class}`orchestrator.workflow.Step`
:param attempts: The maximum amount of times that a provisioning can be retried.
:type attempts: int
: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"))
pp_steps = StepList([_reset_pp_success_state])
for _ in range(attempts):
pp_steps >>= (
should_retry_pp_steps(provisioning_step)
>> should_retry_pp_steps(_await_pp_results)
>> should_retry_pp_steps(_confirm_pp_results)
)
# Abort a workflow if provisioning has failed too many times
pp_steps >>= should_retry_pp_steps(abort)
return pp_steps
......@@ -6,7 +6,7 @@ from gso import settings
# TODO
# - fill in the implementations
# - consider the additional api methods
# - consider the additional API methods
# - decided what to do with various error conditions (currently assertions)
......@@ -31,7 +31,7 @@ _DUMMY_INVENTORY = {
}
def import_new_router(new_router_fqdn: str, oss_params=settings.OSSParams):
def import_new_router(new_router_fqdn: str, subscription_id: str, oss_params=settings.OSSParams):
# TODO: this is a dummy implementation
# TODO: specifiy if this should be an error (and if now, what it means)
......@@ -39,7 +39,7 @@ def import_new_router(new_router_fqdn: str, oss_params=settings.OSSParams):
_DUMMY_INVENTORY[new_router_fqdn] = _dummy_router_interfaces()
def next_lag(router_fqdn: str, oss_params=settings.OSSParams) -> str:
def next_lag(router_fqdn: str, subscription_id: str, oss_params=settings.OSSParams) -> str:
# TODO: this is a dummy implementation
assert router_fqdn in _DUMMY_INVENTORY
......@@ -73,7 +73,9 @@ def _find_physical(router_fqdn: str, interface_name: str) -> dict:
raise AssertionError(f"interface {interface_name} not found on {router_fqdn}")
def reserve_physical_interface(router_fqdn: str, interface_name: str, oss_params=settings.OSSParams):
def reserve_physical_interface(
router_fqdn: str, interface_name: str, subscription_id: str, oss_params=settings.OSSParams
):
# TODO: this is a dummy implementation
ifc = _find_physical(router_fqdn, interface_name)
......
"""GSO settings.
"""{term}`GSO` settings.
Ensuring that the required parameters are set correctly.
"""
......@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
class GeneralParams(BaseSettings):
"""General parameters for a GSO configuration file."""
"""General parameters for a {term}`GSO` configuration file."""
#: The hostname that GSO is publicly served at, used for building the
#: callback URL that the provisioning proxy uses.
public_hostname: str
"""The hostname that {term}`GSO` is publicly served at, used for building the callback URL that the provisioning
proxy uses."""
class InfoBloxParams(BaseSettings):
......@@ -60,7 +60,7 @@ class ServiceNetworkParams(BaseSettings):
class IPAMParams(BaseSettings):
"""A set of parameters related to IPAM."""
"""A set of parameters related to {term}`IPAM`."""
INFOBLOX: InfoBloxParams
LO: ServiceNetworkParams
......@@ -86,7 +86,7 @@ class ResourceManagementParams(BaseSettings):
class OSSParams(BaseSettings):
"""The set of parameters required for running GSO."""
"""The set of parameters required for running {term}`GSO`."""
GENERAL: GeneralParams
IPAM: IPAMParams
......
......@@ -5,6 +5,7 @@
"confirm_info": "Please verify this form looks correct.",
"pp_run_results": "Provisioning proxy results are not ready yet.",
"pp_retry_label": "Playbook execution failure",
"site_bgp_community_id": "Site BGP community ID",
"site_internal_id": "Site internal ID",
......
"""init class that imports all workflows into GSO."""
"""Initialisation class that imports all workflows into {term}`GSO`."""
from orchestrator.workflows import LazyWorkflowInstance
LazyWorkflowInstance("gso.workflows.device.create_device", "create_device")
......
......@@ -18,7 +18,7 @@ from gso.products.product_types import device
from gso.products.product_types.device import DeviceInactive, DeviceProvisioning
from gso.products.product_types.site import Site
from gso.services import _ipam, provisioning_proxy
from gso.services.provisioning_proxy import await_pp_results, confirm_pp_results
from gso.services.provisioning_proxy import pp_interaction
def site_selector() -> Choice:
......@@ -128,11 +128,8 @@ def provision_device_dry(subscription: DeviceProvisioning, process_id: UUIDstr)
return {
"subscription": subscription,
"label_text": (
"Dry run for the deployment of base config on a"
f"new {subscription.device_type}. Deployment is "
"done by the provisioning proxy, please "
"wait for the results to come back before "
"continuing."
f"Dry run for the deployment of base config on a new {subscription.device_type}. Deployment is done by the"
f" provisioning proxy, please wait for the results to come back before continuing."
),
}
......@@ -144,11 +141,8 @@ def provision_device_real(subscription: DeviceProvisioning, process_id: UUIDstr)
return {
"subscription": subscription,
"label_text": (
"Deployment of base config for a new "
f"{subscription.device_type}. Deployment is being "
"taken care of by the provisioning proxy, please "
"wait for the results to come back before "
"continuing."
f"Deployment of base config for a new {subscription.device_type}. Deployment is being taken care of by the"
f" provisioning proxy, please wait for the results to come back before continuing."
),
}
......@@ -165,12 +159,8 @@ def create_device() -> StepList:
>> store_process_subscription(Target.CREATE)
>> initialize_subscription
>> get_info_from_ipam
>> provision_device_dry
>> await_pp_results
>> confirm_pp_results
>> provision_device_real
>> await_pp_results
>> confirm_pp_results
>> pp_interaction(provision_device_dry, 3)
>> pp_interaction(provision_device_real, 3)
>> set_status(SubscriptionLifecycle.ACTIVE)
>> resync
>> done
......
......@@ -20,15 +20,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> InputForm:
@step("Get facts")
def get_facts(subscription_id: UUIDstr) -> dict:
subscription = Device.from_subscription(subscription_id)
# import ansible_runner
#
# r = ansible_runner.run(
# private_data_dir="/opt",
# playbook="get_facts.yaml",
# inventory=subscription.device.device_fqdn,
# )
# out = r.stdout.read()
# out_splitted = out.splitlines()
return {"output": subscription}
......