Skip to content
Snippets Groups Projects
Commit 046c3e1d authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 2.5.

parents f6dc5ba9 f2c7830b
Branches
Tags 2.5
No related merge requests found
Pipeline #87643 passed
Showing
with 431 additions and 14 deletions
......@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## [2.5] - 2024-07-16
- Added import Opengear workflow.
- NAT-616
- NAT-617: Restored ISIS metric to new node
- NAT-619: Fixed BFD update logic bug
## [2.4] - 2024-06-25
- Fixed the issue with client_credentials grant type token in Authentication part.
......
......@@ -17,3 +17,4 @@ Subpackages
router/index
site/index
super_pop_switch/index
opengear/index
``gso.workflows.opengear.create_imported_opengear``
===================================================================
.. automodule:: gso.workflows.opengear.create_imported_opengear
:members:
:show-inheritance:
``gso.workflows.opengear.import_opengear``
==========================================================
.. automodule:: gso.workflows.opengear.import_opengear
:members:
:show-inheritance:
``gso.workflows.opengear``
==================================
.. automodule:: gso.workflows.opengear
:members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 2
:titlesonly:
create_imported_opengear
import_opengear
......@@ -145,8 +145,25 @@ class IptrunkImportModel(BaseModel):
return self
class OpenGearImportModel(BaseModel):
"""Required fields for importing an existing :class:`gso.products.product_types.opengear`."""
partner: str
opengear_site: str
opengear_hostname: str
opengear_wan_address: IPv4AddressType
opengear_wan_netmask: IPv4AddressType
opengear_wan_gateway: IPv4AddressType
T = TypeVar(
"T", SiteImportModel, RouterImportModel, IptrunkImportModel, SuperPopSwitchImportModel, OfficeRouterImportModel
"T",
SiteImportModel,
RouterImportModel,
IptrunkImportModel,
SuperPopSwitchImportModel,
OfficeRouterImportModel,
OpenGearImportModel,
)
common_filepath_option = typer.Option(
......@@ -263,6 +280,18 @@ def import_office_routers(filepath: str = common_filepath_option) -> None:
)
@app.command()
def import_opengear(filepath: str = common_filepath_option) -> None:
"""Import Opengear into GSO."""
_generic_import_product(
Path(filepath),
ProductType.IMPORTED_OPENGEAR,
"opengear",
"opengear_hostname",
OpenGearImportModel,
)
@app.command()
def import_iptrunks(filepath: str = common_filepath_option) -> None:
"""Import IP trunks into GSO."""
......
gso/main.py 100644 → 100755
File mode changed from 100644 to 100755
......@@ -10,9 +10,9 @@ from alembic import op
# revision identifiers, used by Alembic.
revision = 'e8378fbcfbf3'
down_revision = 'da5c9f4cce1c'
branch_labels = None
depends_on = None
down_revision = None
branch_labels = ("data",)
depends_on = "da5c9f4cce1c"
def upgrade() -> None:
......
"""Add upstream migrations as a dependency.
""" Modify ISIS metric workflow description
Revision ID: fvd7mfcfbs1q
Revises:
......
"""Add ImportedOpenGear product..
Revision ID: ccc7ac05063b
Revises: fvd7mfcfbs1q
Create Date: 2024-06-27 11:07:11.122519
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'ccc7ac05063b'
down_revision = 'fvd7mfcfbs1q'
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Opengear', 'Imported Opengear Product', 'ImportedOpengear', 'IMPORTED_OPENGEAR', 'active') RETURNING products.product_id
"""))
conn.execute(sa.text("""
INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OpengearBlock')))
"""))
def downgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OpengearBlock'))
"""))
conn.execute(sa.text("""
DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear'))))
"""))
conn.execute(sa.text("""
DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')))
"""))
conn.execute(sa.text("""
DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear'))
"""))
conn.execute(sa.text("""
DELETE FROM products WHERE products.name IN ('Imported Opengear')
"""))
"""Add import Opengear workflows..
Revision ID: f6a38f9e9e18
Revises: ccc7ac05063b
Create Date: 2024-06-27 11:48:05.331149
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'f6a38f9e9e18'
down_revision = 'ccc7ac05063b'
branch_labels = None
depends_on = None
from orchestrator.migrations.helpers import create_workflow, delete_workflow
new_workflows = [
{
"name": "create_imported_opengear",
"target": "CREATE",
"description": "Import Opengear",
"product_type": "ImportedOpengear"
},
{
"name": "import_opengear",
"target": "MODIFY",
"description": "Import Opengear",
"product_type": "ImportedOpengear"
}
]
def upgrade() -> None:
conn = op.get_bind()
for workflow in new_workflows:
create_workflow(conn, workflow)
def downgrade() -> None:
conn = op.get_bind()
for workflow in new_workflows:
delete_workflow(conn, workflow["name"])
......@@ -11,7 +11,7 @@ from pydantic_forms.types import strEnum
from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect
from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter
from gso.products.product_types.opengear import Opengear
from gso.products.product_types.opengear import ImportedOpengear, Opengear
from gso.products.product_types.pop_vlan import PopVlan
from gso.products.product_types.router import ImportedRouter, Router
from gso.products.product_types.site import ImportedSite, Site
......@@ -36,6 +36,7 @@ class ProductName(strEnum):
IMPORTED_SUPER_POP_SWITCH = "Imported super PoP switch"
IMPORTED_OFFICE_ROUTER = "Imported office router"
OPENGEAR = "Opengear"
IMPORTED_OPENGEAR = "Imported Opengear"
class ProductType(strEnum):
......@@ -55,6 +56,7 @@ class ProductType(strEnum):
IMPORTED_SUPER_POP_SWITCH = ImportedSuperPopSwitch.__name__
IMPORTED_OFFICE_ROUTER = ImportedOfficeRouter.__name__
OPENGEAR = Opengear.__name__
IMPORTED_OPENGEAR = Opengear.__name__
SUBSCRIPTION_MODEL_REGISTRY.update(
......@@ -73,5 +75,6 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
ProductName.IMPORTED_SUPER_POP_SWITCH.value: ImportedSuperPopSwitch,
ProductName.IMPORTED_OFFICE_ROUTER.value: ImportedOfficeRouter,
ProductName.OPENGEAR.value: Opengear,
ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear,
},
)
......@@ -22,3 +22,17 @@ class Opengear(OpengearProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
"""An Opengear that is currently active."""
opengear: OpengearBlock
class ImportedOpengearInactive(SubscriptionModel, is_base=True):
"""An imported, inactive Opengear."""
opengear: OpengearBlockInactive
class ImportedOpengear(
ImportedOpengearInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
):
"""An imported Opengear that is currently active."""
opengear: OpengearBlock
......@@ -59,3 +59,7 @@ LazyWorkflowInstance(
# Office router workflows
LazyWorkflowInstance("gso.workflows.office_router.import_office_router", "import_office_router")
LazyWorkflowInstance("gso.workflows.office_router.create_imported_office_router", "create_imported_office_router")
# Opengear workflows
LazyWorkflowInstance("gso.workflows.opengear.create_imported_opengear", "create_imported_opengear")
LazyWorkflowInstance("gso.workflows.opengear.import_opengear", "import_opengear")
......@@ -494,11 +494,15 @@ def netbox_allocate_side_b_interfaces(subscription: IptrunkInactive) -> None:
@step("Create a new SharePoint checklist item")
def create_new_sharepoint_checklist(subscription: IptrunkProvisioning, tt_number: str) -> State:
def create_new_sharepoint_checklist(subscription: IptrunkProvisioning, tt_number: str, process_id: UUIDstr) -> State:
"""Create a new checklist item in SharePoint for approving this IPtrunk."""
new_list_item_url = SharePointClient().add_list_item(
"ip_trunk",
{"Title": f"{subscription.description} - {subscription.iptrunk.geant_s_sid}", "TT_NUMBER": tt_number},
list_name="ip_trunk",
fields={
"Title": f"{subscription.description} - {subscription.iptrunk.geant_s_sid}",
"TT_NUMBER": tt_number,
"GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
},
)
return {"checklist_url": new_list_item_url}
......
......@@ -25,7 +25,7 @@ from pydantic import AfterValidator, ConfigDict, field_validator
from pydantic_forms.validators import ReadOnlyField, validate_unique_list
from pynetbox.models.dcim import Interfaces
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router
from gso.services import infoblox
......@@ -266,6 +266,34 @@ def check_ip_trunk_optical_levels_post(
return {"subscription": subscription}
@step("Check LLDP on the trunk endpoints")
def check_ip_trunk_lldp(
subscription: Iptrunk,
callback_route: str,
new_node: Router,
new_lag_member_interfaces: list[dict],
replace_index: int,
) -> State:
"""Check LLDP on the new trunk endpoints."""
extra_vars = {
"wfo_ip_trunk_json": json.loads(json_dumps(subscription)),
"new_node": json.loads(json_dumps(new_node)),
"new_lag_member_interfaces": new_lag_member_interfaces,
"replace_index": replace_index,
"check": "lldp",
}
execute_playbook(
playbook_name="iptrunks_checks.yaml",
callback_route=callback_route,
inventory=f"{subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn}\n"
f"{new_node.router.router_fqdn}\n",
extra_vars=extra_vars,
)
return {"subscription": subscription}
@step("[DRY RUN] Disable configuration on old router")
def disable_old_config_dry(
subscription: Iptrunk,
......@@ -416,6 +444,90 @@ def deploy_new_config_real(
return {"subscription": subscription}
@step("[DRY RUN] Update BFD on the remaining side")
def update_remaining_side_bfd_dry(
subscription: Iptrunk,
callback_route: str,
new_node: Router,
replace_index: int,
process_id: UUIDstr,
tt_number: str,
) -> State:
"""Perform a dry run of updating configuration on the remaining router."""
extra_vars = {
"wfo_trunk_json": json.loads(json_dumps(subscription)),
"new_node": json.loads(json_dumps(new_node)),
"replace_index": replace_index,
"verb": "update",
"config_object": "bfd_update",
"dry_run": True,
"commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Update BFD config.",
}
execute_playbook(
playbook_name="iptrunks_migration.yaml",
callback_route=callback_route,
inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn,
extra_vars=extra_vars,
)
return {"subscription": subscription}
@step("[FOR REAL] Update BFD on the remaining side")
def update_remaining_side_bfd_real(
subscription: Iptrunk,
callback_route: str,
new_node: Router,
replace_index: int,
process_id: UUIDstr,
tt_number: str,
) -> State:
"""Update configuration on the remaining router."""
extra_vars = {
"wfo_trunk_json": json.loads(json_dumps(subscription)),
"new_node": json.loads(json_dumps(new_node)),
"replace_index": replace_index,
"verb": "update",
"config_object": "bfd_update",
"dry_run": False,
"commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Update BFD config.",
}
execute_playbook(
playbook_name="iptrunks_migration.yaml",
callback_route=callback_route,
inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn,
extra_vars=extra_vars,
)
return {"subscription": subscription}
@step("Check BFD session over trunk")
def check_ip_trunk_bfd(
subscription: Iptrunk,
callback_route: str,
new_node: Router,
replace_index: int,
) -> State:
"""Check BFD session across the new trunk."""
extra_vars = {
"wfo_ip_trunk_json": json.loads(json_dumps(subscription)),
"new_node": json.loads(json_dumps(new_node)),
"check": "bfd",
}
execute_playbook(
playbook_name="iptrunks_checks.yaml",
callback_route=callback_route,
inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn,
extra_vars=extra_vars,
)
return {"subscription": subscription}
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_move_fiber() -> FormGenerator:
"""Wait for confirmation from an operator that the physical fiber has been moved."""
......@@ -726,6 +838,9 @@ def migrate_iptrunk() -> StepList:
== Vendor.NOKIA
)
should_restore_isis_metric = conditional(lambda state: state["restore_isis_metric"])
trunk_type_is_leased = conditional(
lambda state: state["subscription"]["iptrunk"]["iptrunk_type"] == IptrunkType.LEASED
)
return (
init
......@@ -739,17 +854,21 @@ def migrate_iptrunk() -> StepList:
>> lso_interaction(disable_old_config_real)
>> lso_interaction(deploy_new_config_dry)
>> lso_interaction(deploy_new_config_real)
>> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_dry))
>> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_real))
>> confirm_continue_move_fiber
>> lso_interaction(check_ip_trunk_optical_levels_post)
>> lso_interaction(check_ip_trunk_lldp)
>> trunk_type_is_leased(lso_interaction(check_ip_trunk_bfd))
>> lso_interaction(check_ip_trunk_connectivity)
>> lso_interaction(deploy_new_isis)
>> lso_interaction(check_ip_trunk_isis)
>> should_restore_isis_metric(confirm_continue_restore_isis)
>> should_restore_isis_metric(lso_interaction(restore_isis_metric))
>> lso_interaction(delete_old_config_dry)
>> lso_interaction(delete_old_config_real)
>> update_ipam
>> update_subscription_model
>> should_restore_isis_metric(confirm_continue_restore_isis)
>> should_restore_isis_metric(lso_interaction(restore_isis_metric))
>> old_side_is_nokia(netbox_remove_old_interfaces)
>> new_side_is_nokia(netbox_allocate_new_interfaces)
>> resync
......
"""Workflows related to Opengear subscriptions."""
"""A creation workflow that adds an existing opengear to the service database."""
from orchestrator import workflow
from orchestrator.forms import FormPage
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
from orchestrator.workflow import StepList, done, init, step
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from pydantic import ConfigDict
from gso.products import ProductName
from gso.products.product_types.opengear import ImportedOpengearInactive
from gso.services.partners import get_partner_by_name
from gso.services.subscriptions import get_product_id_by_name, get_site_by_name
from gso.utils.shared_enums import IPv4AddressType
@step("Create subscription")
def create_subscription(partner: str) -> State:
"""Create a new subscription object."""
partner_id = get_partner_by_name(partner)["partner_id"]
product_id = get_product_id_by_name(ProductName.IMPORTED_OPENGEAR)
subscription = ImportedOpengearInactive.from_product_id(product_id, partner_id)
return {
"subscription": subscription,
"subscription_id": subscription.subscription_id,
}
def initial_input_form_generator() -> FormGenerator:
"""Generate a form that is filled in using information passed through the :term:`API` endpoint."""
class ImportOpengear(FormPage):
model_config = ConfigDict(title="Import Opengear")
partner: str
opengear_site: str
opengear_hostname: str
opengear_wan_address: IPv4AddressType
opengear_wan_netmask: IPv4AddressType
opengear_wan_gateway: IPv4AddressType
user_input = yield ImportOpengear
return user_input.dict()
@step("Initialize subscription")
def initialize_subscription(
subscription: ImportedOpengearInactive,
opengear_site: str,
opengear_hostname: str,
opengear_wan_address: IPv4AddressType | None,
opengear_wan_netmask: IPv4AddressType | None,
opengear_wan_gateway: IPv4AddressType | None,
) -> State:
"""Initialise the Imported Opengear subscription using input data."""
subscription.opengear.opengear_site = get_site_by_name(opengear_site).site
subscription.opengear.opengear_hostname = opengear_hostname
subscription.opengear.opengear_wan_address = opengear_wan_address
subscription.opengear.opengear_wan_netmask = opengear_wan_netmask
subscription.opengear.opengear_wan_gateway = opengear_wan_gateway
return {"subscription": subscription}
@workflow(
"Import Opengear",
initial_input_form=initial_input_form_generator,
target=Target.CREATE,
)
def create_imported_opengear() -> StepList:
"""Import an Opengear without provisioning it."""
return (
init
>> create_subscription
>> store_process_subscription(Target.CREATE)
>> initialize_subscription
>> set_status(SubscriptionLifecycle.ACTIVE)
>> resync
>> done
)
"""A modification workflow for migrating an ImportedOpengear to an Opengear subscription."""
from orchestrator.targets import Target
from orchestrator.types import State, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products import ProductName
from gso.products.product_types.opengear import ImportedOpengear, Opengear
from gso.services.subscriptions import get_product_id_by_name
@step("Create new Opengear subscription")
def import_opengear_subscription(subscription_id: UUIDstr) -> State:
"""Take an ImportedOpengear subscription, and turn it into an Opengear subscription."""
old_opengear = ImportedOpengear.from_subscription(subscription_id)
product_id = get_product_id_by_name(ProductName.OPENGEAR)
new_subscription = Opengear.from_other_product(old_opengear, product_id) # type: ignore[arg-type]
return {"subscription": new_subscription}
@workflow("Import Opengear", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
def import_opengear() -> StepList:
"""Modify an ImportedOpengear subscription into an Opengear subscription to complete the import."""
return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_opengear_subscription >> resync >> done
......@@ -21,6 +21,7 @@ from gso.services.lso_client import lso_interaction
from gso.services.netbox_client import NetboxClient
from gso.services.partners import get_partner_by_name
from gso.services.sharepoint import SharePointClient
from gso.settings import load_oss_params
from gso.utils.helpers import generate_fqdn, iso_from_ipv4
from gso.utils.shared_enums import PortNumber, Vendor
from gso.utils.workflow_steps import (
......@@ -224,10 +225,15 @@ def prompt_insert_in_radius(subscription: RouterInactive) -> FormGenerator:
@step("Create a new SharePoint checklist")
def create_new_sharepoint_checklist(subscription: RouterProvisioning, tt_number: str) -> State:
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(
"p_router", {"Title": subscription.router.router_fqdn, "TT_NUMBER": tt_number}
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}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment