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 (
from gso.settings import IPAMParams, load_oss_params
logger = getLogger(__name__)
NULL_MAC = "00:00:00:00:00:00"
class AllocationError(Exception):
......@@ -40,11 +41,7 @@ def _setup_connection() -> tuple[connector.Connector, IPAMParams]:
def _allocate_network(
conn: connector.Connector,
dns_view: str,
netmask: int,
containers: list[str],
comment: str | None = "",
conn: connector.Connector, dns_view: str, netmask: int, containers: list[str], comment: str | None = ""
) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
"""Allocate a new network in Infoblox.
......@@ -160,10 +157,7 @@ def delete_network(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network) ->
def allocate_host(
hostname: str,
service_type: str,
cname_aliases: list[str],
comment: str,
hostname: str, service_type: str, cname_aliases: list[str], comment: str
) -> tuple[ipaddress.IPv4Address, ipaddress.IPv6Address]:
"""Allocate a new host record in Infoblox.
......@@ -194,7 +188,7 @@ def allocate_host(
created_v6 = None
for ipv6_range in allocation_networks_v6:
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:
new_host = objects.HostRecord.create(
conn,
......@@ -216,7 +210,7 @@ def allocate_host(
created_v4 = None
for ipv4_range in allocation_networks_v4:
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.ipv4addrs = [ipv4_object]
try:
......@@ -234,9 +228,39 @@ def allocate_host(
return created_v4, created_v6
def find_host_by_ip(
ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
) -> objects.HostRecord | None:
def create_host_by_ip(
hostname: str,
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.
:param ip_addr: The IP address of a host that is searched for.
......@@ -249,14 +273,14 @@ def find_host_by_ip(
ipv4addr=ip_addr,
return_fields=["ipv4addrs", "name", "view", "aliases", "comment"],
)
return objects.HostRecord.search(
return objects.HostRecordV6.search(
conn,
ipv6addr=ip_addr,
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`.
: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:
)
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:
"""Delete a host from Infoblox.
......
......@@ -16,6 +16,7 @@ from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.utils.errors import ProcessFailureError
from orchestrator.utils.json import json_dumps
from orchestrator.workflow import StepList, conditional, done, init, inputstep
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
......@@ -23,11 +24,13 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import validator
from pydantic_forms.core import ReadOnlyField
from pynetbox.models.dcim import Interfaces
from services.infoblox import DeletionError
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router
from gso.services import infoblox
from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import execute_playbook, pp_interaction
from gso.services.subscriptions import get_active_router_subscriptions
......@@ -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")
def disable_old_config_dry(
subscription: Iptrunk,
......@@ -488,12 +535,28 @@ def delete_old_config_real(
return {"subscription": subscription}
@step("Update IPAM")
def update_ipam(subscription: Iptrunk) -> State:
@step("Update IP records in IPAM")
def update_ipam(subscription: Iptrunk, old_side_data: dict, new_node: Router, new_lag_interface: str) -> State:
"""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}
......@@ -507,12 +570,6 @@ def update_subscription_model(
) -> State:
"""Update the subscription model in the database."""
# 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_ae_iface = new_lag_interface
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.clear()
......@@ -522,40 +579,6 @@ def update_subscription_model(
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}
......@@ -628,6 +651,7 @@ def migrate_iptrunk() -> StepList:
>> store_process_subscription(Target.MODIFY)
>> unsync
>> new_side_is_nokia(netbox_reserve_interfaces)
>> calculate_old_side_data
>> pp_interaction(set_isis_to_90000)
>> pp_interaction(disable_old_config_dry)
>> pp_interaction(disable_old_config_real)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment