Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IPv6 address support to linknets #296

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Add IPv6 address fields to linknets

Revision ID: 4ec3a80231f8
Revises: b7629362583c
Create Date: 2023-03-10 15:45:27.507737

"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils


# revision identifiers, used by Alembic.
revision = '4ec3a80231f8'
down_revision = 'b7629362583c'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('linknet', sa.Column('ipv6_network', sa.Unicode(length=43), nullable=True))
op.add_column('linknet', sa.Column('device_a_ipv6', sqlalchemy_utils.types.ip_address.IPAddressType(length=50), nullable=True))
op.add_column('linknet', sa.Column('device_b_ipv6', sqlalchemy_utils.types.ip_address.IPAddressType(length=50), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('linknet', 'device_b_ipv6')
op.drop_column('linknet', 'device_a_ipv6')
op.drop_column('linknet', 'ipv6_network')
# ### end Alembic commands ###
54 changes: 52 additions & 2 deletions src/cnaas_nms/api/linknet.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ipaddress import IPv4Address, IPv4Network
from ipaddress import IPv4Address, IPv4Network, IPv6Network, IPv6Address
from typing import Optional

from flask import request
Expand Down Expand Up @@ -27,6 +27,7 @@
"device_a_port": fields.String(required=True),
"device_b_port": fields.String(required=True),
"ipv4_network": fields.String(required=False),
"ipv6_network": fields.String(required=False),
},
)

Expand All @@ -39,16 +40,22 @@
"device_a_port": fields.String(required=False),
"device_b_port": fields.String(required=False),
"ipv4_network": fields.String(required=False),
"ipv6_network": fields.String(required=False),
"device_a_ip": fields.String(required=False),
"device_a_ipv6": fields.String(required=False),
"device_b_ip": fields.String(required=False),
"device_b_ipv6": fields.String(required=False),
},
)


class f_linknet(BaseModel):
ipv4_network: Optional[str] = None
ipv6_network: Optional[str] = None
device_a_ip: Optional[str] = None
device_a_ipv6: Optional[str] = None
device_b_ip: Optional[str] = None
device_b_ipv6: Optional[str] = None

@validator("device_a_ip", "device_b_ip")
def device_ip_validator(cls, v, values, **kwargs):
Expand All @@ -71,6 +78,27 @@ def device_ip_validator(cls, v, values, **kwargs):

return v

@validator("device_a_ipv6", "device_b_ipv6")
def device_ipv6_validator(cls, v, values, **kwargs):
if not v:
return v
if not values["ipv6_network"]:
raise ValueError("ipv6_network must be set")
try:
addr = IPv6Address(v)
net = IPv6Network(values["ipv6_network"])
except Exception: # noqa: S110
raise ValueError("Invalid device IP or ipv6_network")
else:
if addr not in net:
raise ValueError("device IP must be part of ipv6_network")
if "device_a_ipv6" in values and v == values["device_a_ipv6"]:
raise ValueError("device_a_ipv6 and device_b_ipv6 can not be the same")
if "device_b_ipv6" in values and v == values["device_b_ipv6"]:
raise ValueError("device_a_ipv6 and device_b_ipv6 can not be the same")

return v

@validator("ipv4_network")
def ipv4_network_validator(cls, v, values, **kwargs):
if not v:
Expand All @@ -86,6 +114,20 @@ def ipv4_network_validator(cls, v, values, **kwargs):

return v

@validator("ipv6_network")
def ipv6_network_validator(cls, v, values, **kwargs):
if not v:
return v
try:
net = IPv6Network(v)
prefix_len = int(net.prefixlen)
except Exception: # noqa: S110
raise ValueError("Invalid ipv6_network received. Must be IPv6 network address with mask")
else:
if prefix_len < 64:
raise ValueError("Bad prefix length {} for linknet IPv6 network".format(prefix_len))

return v

class LinknetsApi(Resource):
@staticmethod
Expand Down Expand Up @@ -138,6 +180,13 @@ def post(self):
except Exception as e:
errors.append("Invalid ipv4_network: {}".format(e))

new_prefix_v6 = None
if json_data.get("ipv6_network"):
try:
new_prefix_v6 = IPv6Network(json_data["ipv6_network"])
except Exception as e:
errors.append("Invalid ipv6_network: {}".format(e))

if errors:
return empty_result(status="error", data=errors), 400

Expand Down Expand Up @@ -170,7 +219,8 @@ def post(self):
json_data["device_a_port"],
json_data["device_b"],
json_data["device_b_port"],
new_prefix,
ipv4_network=new_prefix,
ipv6_network=new_prefix_v6,
)
session.add(new_linknet)
session.commit()
Expand Down
40 changes: 35 additions & 5 deletions src/cnaas_nms/db/linknet.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import enum
import ipaddress
import itertools
from typing import List, Optional

from sqlalchemy import Column, ForeignKey, Integer, Unicode, UniqueConstraint
Expand All @@ -21,17 +22,20 @@ class Linknet(cnaas_nms.db.base.Base):
)
id = Column(Integer, autoincrement=True, primary_key=True)
ipv4_network = Column(Unicode(18))
ipv6_network = Column(Unicode(43))
device_a_id = Column(Integer, ForeignKey("device.id"))
device_a = relationship(
"Device", foreign_keys=[device_a_id], backref=backref("linknets_a", cascade="all, delete-orphan")
)
device_a_ip = Column(IPAddressType)
device_a_ipv6 = Column(IPAddressType)
device_a_port = Column(Unicode(64))
device_b_id = Column(Integer, ForeignKey("device.id"))
device_b = relationship(
"Device", foreign_keys=[device_b_id], backref=backref("linknets_b", cascade="all, delete-orphan")
)
device_b_ip = Column(IPAddressType)
device_b_ipv6 = Column(IPAddressType)
device_b_port = Column(Unicode(64))
site_id = Column(Integer, ForeignKey("site.id"))
site = relationship("Site")
Expand Down Expand Up @@ -67,11 +71,29 @@ def get_ip(self, device_id):
return self.device_b_ip
raise ValueError(f"The device_id {device_id} is not part of this linknet")

def get_ipv6(self, device_id):
if device_id == self.device_a_id:
return self.device_a_ipv6
if device_id == self.device_b_id:
return self.device_b_ipv6
raise ValueError(f"The device_id {device_id} is not part of this linknet")

def get_ipif(self, device_id):
prefixlen = ipaddress.IPv4Network(self.ipv4_network).prefixlen
if device_id == self.device_a_id:
return f"{self.device_a_ip}/{prefixlen}"
if device_id == self.device_b_id:
return f"{self.device_b_ip}/{prefixlen}"
raise ValueError(f"The device_id {device_id} is not part of this linknet")

def get_ipifv6(self, device_id):
if not self.ipv6_network:
return # we treat ipv6 as optional (for now)
prefixlen = ipaddress.IPv6Network(self.ipv6_network).prefixlen
if device_id == self.device_a_id:
return f"{self.device_a_ip}/{ipaddress.IPv4Network(self.ipv4_network).prefixlen}"
return f"{self.device_a_ip}/{prefixlen}"
if device_id == self.device_b_id:
return f"{self.device_b_ip}/{ipaddress.IPv4Network(self.ipv4_network).prefixlen}"
return f"{self.device_b_ip}/{prefixlen}"
raise ValueError(f"The device_id {device_id} is not part of this linknet")

@staticmethod
Expand Down Expand Up @@ -114,9 +136,10 @@ def create_linknet(
hostname_b: str,
interface_b: str,
ipv4_network: Optional[ipaddress.IPv4Network] = None,
ipv6_network: Optional[ipaddress.IPv6Network] = None,
strict_check: bool = True,
):
"""Add a linknet between two devices. If ipv4_network is specified both
"""Add a linknet between two devices. If ipv4_network/ipv6_network is specified both
devices must be of type CORE or DIST."""
dev_a: cnaas_nms.db.device.Device = (
session.query(cnaas_nms.db.device.Device)
Expand All @@ -127,7 +150,7 @@ def create_linknet(
raise ValueError(f"Hostname {hostname_a} not found in database")
if (
strict_check
and ipv4_network
and (ipv4_network or ipv6_network)
and dev_a.device_type not in [cnaas_nms.db.device.DeviceType.DIST, cnaas_nms.db.device.DeviceType.CORE]
):
raise ValueError(
Expand All @@ -143,7 +166,7 @@ def create_linknet(
raise ValueError(f"Hostname {hostname_b} not found in database")
if (
strict_check
and ipv4_network
and (ipv4_network or ipv6_network)
and dev_b.device_type not in [cnaas_nms.db.device.DeviceType.DIST, cnaas_nms.db.device.DeviceType.CORE]
):
raise ValueError(
Expand All @@ -163,6 +186,13 @@ def create_linknet(
new_linknet.device_a_ip = ip_a
new_linknet.device_b_ip = ip_b
new_linknet.ipv4_network = str(ipv4_network)
if ipv6_network:
if not isinstance(ipv6_network, ipaddress.IPv6Network):
raise ValueError("IPv6 Linknet must be an IPv6Network")
ip_a, ip_b = itertools.islice(ipv6_network.hosts(), 2)
new_linknet.device_a_ipv6 = str(ip_a)
new_linknet.device_b_ipv6 = str(ip_b)
new_linknet.ipv6_network = str(ipv6_network)
if strict_check:
dev_a.synchronized = False
dev_b.synchronized = False
Expand Down
6 changes: 6 additions & 0 deletions src/cnaas_nms/devicehandler/sync_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,19 @@ def populate_device_vars(
for linknet in dev.get_links_to(session, neighbor_d):
local_if = linknet.get_port(dev.id)
local_ipif = linknet.get_ipif(dev.id)
local_ipifv6 = linknet.get_ipifv6(dev.id)
neighbor_ip = linknet.get_ip(neighbor_d.id)
neighbor_ipv6 = linknet.get_ipv6(neighbor_d.id)
if local_if:
fabric_interfaces[local_if] = {
"name": local_if,
"ifclass": "fabric",
"ipv4if": local_ipif,
"ipv6if": local_ipifv6,
"peer_hostname": neighbor_d.hostname,
"peer_infra_lo": str(neighbor_d.infra_ip),
"peer_ip": str(neighbor_ip),
"peer_ipv6": str(neighbor_ipv6),
"peer_asn": generate_asn(neighbor_d.infra_ip),
}
fabric_device_variables["bgp_ipv4_peers"].append(
Expand Down Expand Up @@ -291,9 +295,11 @@ def populate_device_vars(
"ifclass": intf["ifclass"],
"indexnum": ifindexnum,
"ipv4if": None,
"ipv6if": None,
"peer_hostname": "ztp",
"peer_infra_lo": None,
"peer_ip": None,
"peer_ipv6": None,
"peer_asn": None,
}
)
Expand Down
17 changes: 17 additions & 0 deletions src/cnaas_nms/devicehandler/tests/data/testdata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,47 @@ linknet_redundant:
- description: null
device_a_hostname: eosaccess
device_a_ip: null
device_a_ipv6: null
device_a_id: null
device_a_port: Ethernet2
device_b_hostname: eosdist1
device_b_id: null
device_b_ip: null
device_b_ipv6: null
device_b_port: Ethernet2
ipv4_network: null
ipv6_network: null
redundant_link: true
site_id: null
- description: null
device_a_hostname: eosaccess
device_a_ip: null
device_a_ipv6: null
device_a_id: null
device_a_port: Ethernet3
device_b_hostname: eosdist2
device_b_ip: null
device_b_ipv6: null
device_b_id: null
device_b_port: Ethernet2
ipv4_network: null
ipv6_network: null
redundant_link: true
site_id: null
linknet_nonredundant:
- description: null
device_a_hostname: eosaccess
device_a_ip: null
device_a_ipv6: null
device_a_id: null
device_a_port: Ethernet2
device_b_hostname: eosdist1
device_b_ip: null
device_b_ipv6: null
device_b_id: null
device_b_port: Ethernet20
ipv4_network: null
ipv6_network: null
redundant_link: false
site_id: null
lldp_data_redundant:
Expand All @@ -62,25 +71,33 @@ mlag_dev_nonpeer: nonpeer
linknets_mlag_peers:
- description: null
device_a_ip: null
device_a_ipv6: null
device_a_port: Ethernet25
device_b_ip: null
device_b_ipv6: null
device_b_port: Ethernet51
ipv4_network: null
ipv6_network: null
redundant_link: true
site_id: null
- description: null
device_a_ip: null
device_a_ipv6: null
device_a_port: Ethernet26
device_b_ip: null
device_b_ipv6: null
device_b_port: Ethernet52
ipv4_network: null
ipv6_network: null
redundant_link: true
site_id: null
linknets_mlag_nonpeers:
- description: null
device_a_ip: null
device_a_ipv6: null
device_a_port: Ethernet20
device_b_ip: null
device_b_ipv6: null
device_b_port: Ethernet20
ipv4_network: null
redundant_link: true
Expand Down