Skip to content
Snippets Groups Projects
Commit b980ff4a authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

Update infoblox service and add IPAM step to migrate IP trunk workflow

parent ac9b297a
No related branches found
No related tags found
1 merge request!135Feature/trunk migration ipam
...@@ -12,6 +12,7 @@ from infoblox_client.exceptions import ( ...@@ -12,6 +12,7 @@ from infoblox_client.exceptions import (
from gso.settings import IPAMParams, load_oss_params from gso.settings import IPAMParams, load_oss_params
logger = getLogger(__name__) logger = getLogger(__name__)
NULL_MAC = "00:00:00:00:00:00"
class AllocationError(Exception): class AllocationError(Exception):
...@@ -40,11 +41,7 @@ def _setup_connection() -> tuple[connector.Connector, IPAMParams]: ...@@ -40,11 +41,7 @@ def _setup_connection() -> tuple[connector.Connector, IPAMParams]:
def _allocate_network( def _allocate_network(
conn: connector.Connector, conn: connector.Connector, dns_view: str, netmask: int, containers: list[str], comment: str | None = ""
dns_view: str,
netmask: int,
containers: list[str],
comment: str | None = "",
) -> ipaddress.IPv4Network | ipaddress.IPv6Network: ) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
"""Allocate a new network in Infoblox. """Allocate a new network in Infoblox.
...@@ -160,10 +157,7 @@ def delete_network(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network) -> ...@@ -160,10 +157,7 @@ def delete_network(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network) ->
def allocate_host( def allocate_host(
hostname: str, hostname: str, service_type: str, cname_aliases: list[str], comment: str
service_type: str,
cname_aliases: list[str],
comment: str,
) -> tuple[ipaddress.IPv4Address, ipaddress.IPv6Address]: ) -> tuple[ipaddress.IPv4Address, ipaddress.IPv6Address]:
"""Allocate a new host record in Infoblox. """Allocate a new host record in Infoblox.
...@@ -194,7 +188,7 @@ def allocate_host( ...@@ -194,7 +188,7 @@ def allocate_host(
created_v6 = None created_v6 = None
for ipv6_range in allocation_networks_v6: for ipv6_range in allocation_networks_v6:
v6_alloc = objects.IPAllocation.next_available_ip_from_cidr(dns_view, str(ipv6_range)) v6_alloc = objects.IPAllocation.next_available_ip_from_cidr(dns_view, str(ipv6_range))
ipv6_object = objects.IP.create(ip=v6_alloc, mac="00:00:00:00:00:00", configure_for_dhcp=False) ipv6_object = objects.IP.create(ip=v6_alloc, mac=NULL_MAC, configure_for_dhcp=False)
try: try:
new_host = objects.HostRecord.create( new_host = objects.HostRecord.create(
conn, conn,
...@@ -216,7 +210,7 @@ def allocate_host( ...@@ -216,7 +210,7 @@ def allocate_host(
created_v4 = None created_v4 = None
for ipv4_range in allocation_networks_v4: for ipv4_range in allocation_networks_v4:
v4_alloc = objects.IPAllocation.next_available_ip_from_cidr(dns_view, str(ipv4_range)) v4_alloc = objects.IPAllocation.next_available_ip_from_cidr(dns_view, str(ipv4_range))
ipv4_object = objects.IP.create(ip=v4_alloc, mac="00:00:00:00:00:00", configure_for_dhcp=False) ipv4_object = objects.IP.create(ip=v4_alloc, mac=NULL_MAC, configure_for_dhcp=False)
new_host = objects.HostRecord.search(conn, name=hostname) new_host = objects.HostRecord.search(conn, name=hostname)
new_host.ipv4addrs = [ipv4_object] new_host.ipv4addrs = [ipv4_object]
try: try:
...@@ -234,9 +228,39 @@ def allocate_host( ...@@ -234,9 +228,39 @@ def allocate_host(
return created_v4, created_v6 return created_v4, created_v6
def find_host_by_ip( def create_host_by_ip(
ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address, hostname: str,
) -> objects.HostRecord | None: ipv4_address: ipaddress.IPv4Address,
ipv6_address: ipaddress.IPv6Address,
service_type: str,
comment: str,
) -> None:
"""Create a new host record with a given IPv4 and IPv6 address.
:param str hostname: The :term:`FQDN` of the new host.
:param IPv4Address ipv4_address: The IPv4 address of the new host.
:param IPv6Address ipv6_address: The IPv6 address of the new host.
:param str service_type: The relevant service type, used to deduce the correct ``dns_view`` in Infoblox.
:param str comment: The comment stored in this Infoblox record, most likely the relevant ``subscription_id`` in
:term:`GSO`.
"""
if not hostname_available(hostname):
msg = f"Cannot allocate new host, FQDN {hostname} already taken."
raise AllocationError(msg)
conn, oss = _setup_connection()
ipv6_object = objects.IP.create(ip=ipv6_address, mac=NULL_MAC, configure_for_dhcp=False)
ipv4_object = objects.IP.create(ip=ipv4_address, mac=NULL_MAC, configure_for_dhcp=False)
dns_view = getattr(oss, service_type).dns_view
# This needs to be done in two steps, otherwise only one of the IP addresses is stored.
objects.HostRecord.create(conn, ip=ipv6_object, name=hostname, comment=comment, dns_view=dns_view)
new_host = find_host_by_fqdn(hostname)
new_host.ipv4addrs = [ipv4_object]
new_host.update()
def find_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> objects.HostRecord | None:
"""Find a host record in Infoblox by its associated IP address. """Find a host record in Infoblox by its associated IP address.
:param ip_addr: The IP address of a host that is searched for. :param ip_addr: The IP address of a host that is searched for.
...@@ -249,14 +273,14 @@ def find_host_by_ip( ...@@ -249,14 +273,14 @@ def find_host_by_ip(
ipv4addr=ip_addr, ipv4addr=ip_addr,
return_fields=["ipv4addrs", "name", "view", "aliases", "comment"], return_fields=["ipv4addrs", "name", "view", "aliases", "comment"],
) )
return objects.HostRecord.search( return objects.HostRecordV6.search(
conn, conn,
ipv6addr=ip_addr, ipv6addr=ip_addr,
return_fields=["ipv6addrs", "name", "view", "aliases", "comment"], return_fields=["ipv6addrs", "name", "view", "aliases", "comment"],
) )
def find_host_by_fqdn(fqdn: str) -> objects.HostRecord | None: def find_host_by_fqdn(fqdn: str) -> objects.HostRecord:
"""Find a host record by its associated :term:`FQDN`. """Find a host record by its associated :term:`FQDN`.
:param fqdn: The :term:`FQDN` of a host that is searched for. :param fqdn: The :term:`FQDN` of a host that is searched for.
...@@ -270,6 +294,18 @@ def find_host_by_fqdn(fqdn: str) -> objects.HostRecord | None: ...@@ -270,6 +294,18 @@ def find_host_by_fqdn(fqdn: str) -> objects.HostRecord | None:
) )
def find_v6_host_by_fqdn(fqdn: str) -> objects.HostRecordV6:
"""Find a host record by its associated :term:`FQDN`.
This specific method will return the IPv6 variant of a record, if it exists.
:param str fqdn: The :term:`FQDN` of a host that is searched for.
"""
conn, _ = _setup_connection()
return objects.HostRecordV6.search(
conn, name=fqdn, return_fields=["ipv6addrs", "name", "view", "aliases", "comment"]
)
def delete_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> None: def delete_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> None:
"""Delete a host from Infoblox. """Delete a host from Infoblox.
......
...@@ -16,6 +16,7 @@ from orchestrator.forms import FormPage ...@@ -16,6 +16,7 @@ from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList
from orchestrator.targets import Target from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.utils.errors import ProcessFailureError
from orchestrator.utils.json import json_dumps from orchestrator.utils.json import json_dumps
from orchestrator.workflow import StepList, conditional, done, init, inputstep from orchestrator.workflow import StepList, conditional, done, init, inputstep
from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.steps import resync, store_process_subscription, unsync
...@@ -23,11 +24,13 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form ...@@ -23,11 +24,13 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import validator from pydantic import validator
from pydantic_forms.core import ReadOnlyField from pydantic_forms.core import ReadOnlyField
from pynetbox.models.dcim import Interfaces from pynetbox.models.dcim import Interfaces
from services.infoblox import DeletionError
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
from gso.services import infoblox
from gso.services.netbox_client import NetboxClient from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.services.provisioning_proxy import execute_playbook, pp_interaction
from gso.services.subscriptions import get_active_router_subscriptions from gso.services.subscriptions import get_active_router_subscriptions
...@@ -167,6 +170,50 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -167,6 +170,50 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
) )
@step("Netbox: Reserve new interfaces")
def netbox_reserve_interfaces(
subscription: Iptrunk, new_node: UUIDstr, new_lag_interface: str, new_lag_member_interfaces: list[dict]
) -> State:
"""Reserve new interfaces in Netbox, only when the new side's router is a NOKIA router."""
new_side = Router.from_subscription(new_node).router
nbclient = NetboxClient()
# Create :term:`LAG` interfaces
lag_interface: Interfaces = nbclient.create_interface(
iface_name=new_lag_interface,
interface_type="lag",
device_name=new_side.router_fqdn,
description=str(subscription.subscription_id),
enabled=True,
)
# Attach physical interfaces to :term:`LAG`
# Reserve interfaces
for interface in new_lag_member_interfaces:
nbclient.attach_interface_to_lag(
device_name=new_side.router_fqdn,
lag_name=lag_interface.name,
iface_name=interface["interface_name"],
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=new_side.router_fqdn,
iface_name=interface["interface_name"],
)
return {"subscription": subscription}
@step("Calculate old side data")
def calculate_old_side_data(subscription: Iptrunk, replace_index: int) -> State:
"""Store subscription information of the old side in the state of the workflow for later use."""
old_subscription = copy.deepcopy(subscription)
old_side_data = {
"iptrunk_side_node": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node,
"iptrunk_side_ae_iface": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface,
"iptrunk_side_ae_members": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members,
}
return {"old_side_data": old_side_data}
@step("[DRY RUN] Disable configuration on old router") @step("[DRY RUN] Disable configuration on old router")
def disable_old_config_dry( def disable_old_config_dry(
subscription: Iptrunk, subscription: Iptrunk,
...@@ -488,12 +535,28 @@ def delete_old_config_real( ...@@ -488,12 +535,28 @@ def delete_old_config_real(
return {"subscription": subscription} return {"subscription": subscription}
@step("Update IPAM") @step("Update IP records in IPAM")
def update_ipam(subscription: Iptrunk) -> State: def update_ipam(subscription: Iptrunk, old_side_data: dict, new_node: Router, new_lag_interface: str) -> State:
"""Update :term:`IPAM` resources. """Update :term:`IPAM` resources.
TODO: implement Move the DNS record pointing to the old side of the trunk, to the new side.
""" """
old_fqdn = f"{old_side_data['iptrunk_side_ae_iface']}.{old_side_data['iptrunk_side_node']['router_fqdn']}"
trunk_v4 = infoblox.find_host_by_fqdn(old_fqdn)
trunk_v6 = infoblox.find_v6_host_by_fqdn(old_fqdn)
# Out with the old
try:
infoblox.delete_host_by_fqdn(old_fqdn)
except DeletionError as e:
msg = "Failed to delete record from Infoblox."
raise ProcessFailureError(msg) from e
# And in with the new
new_fqdn = f"{new_lag_interface}.{new_node.router.router_fqdn}"
comment = str(subscription.subscription_id)
infoblox.create_host_by_ip(new_fqdn, trunk_v4.ipv4addr, trunk_v6.ipv6addr, service_type="TRUNK", comment=comment)
return {"subscription": subscription} return {"subscription": subscription}
...@@ -507,12 +570,6 @@ def update_subscription_model( ...@@ -507,12 +570,6 @@ def update_subscription_model(
) -> State: ) -> State:
"""Update the subscription model in the database.""" """Update the subscription model in the database."""
# Deep copy of subscription data # Deep copy of subscription data
old_subscription = copy.deepcopy(subscription)
old_side_data = {
"iptrunk_side_node": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node,
"iptrunk_side_ae_iface": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface,
"iptrunk_side_ae_members": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members,
}
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.clear() subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.clear()
...@@ -522,40 +579,6 @@ def update_subscription_model( ...@@ -522,40 +579,6 @@ def update_subscription_model(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member), IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member),
) )
return {"subscription": subscription, "old_side_data": old_side_data}
@step("Netbox: Reserve new interfaces")
def netbox_reserve_interfaces(
subscription: Iptrunk,
new_node: UUIDstr,
new_lag_interface: str,
new_lag_member_interfaces: list[dict],
) -> State:
"""Reserve new interfaces in Netbox, only when the new side's router is a NOKIA router."""
new_side = Router.from_subscription(new_node).router
nbclient = NetboxClient()
# Create :term:`LAG` interfaces
lag_interface: Interfaces = nbclient.create_interface(
iface_name=new_lag_interface,
interface_type="lag",
device_name=new_side.router_fqdn,
description=str(subscription.subscription_id),
enabled=True,
)
# Attach physical interfaces to :term:`LAG`
# Reserve interfaces
for interface in new_lag_member_interfaces:
nbclient.attach_interface_to_lag(
device_name=new_side.router_fqdn,
lag_name=lag_interface.name,
iface_name=interface["interface_name"],
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=new_side.router_fqdn,
iface_name=interface["interface_name"],
)
return {"subscription": subscription} return {"subscription": subscription}
...@@ -628,6 +651,7 @@ def migrate_iptrunk() -> StepList: ...@@ -628,6 +651,7 @@ def migrate_iptrunk() -> StepList:
>> store_process_subscription(Target.MODIFY) >> store_process_subscription(Target.MODIFY)
>> unsync >> unsync
>> new_side_is_nokia(netbox_reserve_interfaces) >> new_side_is_nokia(netbox_reserve_interfaces)
>> calculate_old_side_data
>> pp_interaction(set_isis_to_90000) >> pp_interaction(set_isis_to_90000)
>> pp_interaction(disable_old_config_dry) >> pp_interaction(disable_old_config_dry)
>> pp_interaction(disable_old_config_real) >> pp_interaction(disable_old_config_real)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment